public inbox for [email protected]
help / color / mirror / Atom feed[PATCH v23 5/5] Additional test for new GUC setting.
28+ messages / 6 participants
[nested] [flat]
* [PATCH v23 5/5] Additional test for new GUC setting.
@ 2019-08-28 05:12 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Kyotaro Horiguchi @ 2019-08-28 05:12 UTC (permalink / raw)
This patchset adds new GUC variable effective_io_block_size that
controls wheter WAL-skipped tables are finally WAL-logged or
fcync'ed. All of the TAP test performs WAL-logging so this adds an
item that performs file sync.
---
src/test/recovery/t/018_wal_optimize.pl | 38 ++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index ac62c77a42..470d4f048c 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -18,7 +18,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 26;
+use Test::More tests => 32;
# Make sure no orphan relfilenode files exist.
sub check_orphan_relfilenodes
@@ -52,6 +52,8 @@ sub run_wal_optimize
$node->append_conf('postgresql.conf', qq(
wal_level = $wal_level
max_prepared_transactions = 1
+wal_log_hints = on
+wal_skip_threshold = 0
));
$node->start;
@@ -111,7 +113,23 @@ max_prepared_transactions = 1
$result = $node->safe_psql('postgres', "SELECT count(*) FROM test2a;");
is($result, qq(1),
"wal_level = $wal_level, optimized truncation with prepared transaction");
+ # Same for file sync mode
+ # Tuples inserted after the truncation should be seen.
+ $node->safe_psql('postgres', "
+ SET wal_skip_threshold to 0;
+ BEGIN;
+ CREATE TABLE test2b (id serial PRIMARY KEY);
+ INSERT INTO test2b VALUES (DEFAULT);
+ TRUNCATE test2b;
+ INSERT INTO test2b VALUES (DEFAULT);
+ COMMIT;");
+
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->safe_psql('postgres', "SELECT count(*) FROM test2b;");
+ is($result, qq(1),
+ "wal_level = $wal_level, optimized truncation with file-sync");
# Data file for COPY query in follow-up tests.
my $basedir = $node->basedir;
@@ -187,6 +205,24 @@ max_prepared_transactions = 1
is($result, qq(3),
"wal_level = $wal_level, SET TABLESPACE in subtransaction");
+ $node->safe_psql('postgres', "
+ BEGIN;
+ CREATE TABLE test3a5 (c int PRIMARY KEY);
+ SAVEPOINT q; INSERT INTO test3a5 VALUES (1); ROLLBACK TO q;
+ CHECKPOINT;
+ INSERT INTO test3a5 VALUES (1); -- set index hint bit
+ INSERT INTO test3a5 VALUES (2);
+ COMMIT;");
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->psql('postgres', );
+ my($ret, $stdout, $stderr) = $node->psql(
+ 'postgres', "INSERT INTO test3a5 VALUES (2);");
+ is($ret, qq(3),
+ "wal_level = $wal_level, unique index LP_DEAD");
+ like($stderr, qr/violates unique/,
+ "wal_level = $wal_level, unique index LP_DEAD message");
+
# UPDATE touches two buffers; one is BufferNeedsWAL(); the other is not.
$node->safe_psql('postgres', "
BEGIN;
--
2.23.0
----Next_Part(Wed_Nov__6_17_29_27_2019_575)----
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 4/4] Additional test for new GUC setting.
@ 2019-08-28 05:12 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Kyotaro Horiguchi @ 2019-08-28 05:12 UTC (permalink / raw)
This patchset adds new GUC variable effective_io_block_size that
controls wheter WAL-skipped tables are finally WAL-logged or
fcync'ed. All of the TAP test performs WAL-logging so this adds an
item that performs file sync.
---
src/test/recovery/t/018_wal_optimize.pl | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index b041121745..95063ab131 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -11,7 +11,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 26;
+use Test::More tests => 28;
sub check_orphan_relfilenodes
{
@@ -102,7 +102,23 @@ max_prepared_transactions = 1
$result = $node->safe_psql('postgres', "SELECT count(*) FROM test2a;");
is($result, qq(1),
"wal_level = $wal_level, optimized truncation with prepared transaction");
+ # Same for file sync mode
+ # Tuples inserted after the truncation should be seen.
+ $node->safe_psql('postgres', "
+ SET effective_io_block_size to 0;
+ BEGIN;
+ CREATE TABLE test2b (id serial PRIMARY KEY);
+ INSERT INTO test2b VALUES (DEFAULT);
+ TRUNCATE test2b;
+ INSERT INTO test2b VALUES (DEFAULT);
+ COMMIT;");
+ $node->stop('immediate');
+ $node->start;
+
+ $result = $node->safe_psql('postgres', "SELECT count(*) FROM test2b;");
+ is($result, qq(1),
+ "wal_level = $wal_level, optimized truncation with file-sync");
# Data file for COPY query in follow-up tests.
my $basedir = $node->basedir;
--
2.16.3
----Next_Part(Wed_Aug_28_15_42_10_2019_037)----
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v21 5/5] Additional test for new GUC setting.
@ 2019-08-28 05:12 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Kyotaro Horiguchi @ 2019-08-28 05:12 UTC (permalink / raw)
This patchset adds new GUC variable effective_io_block_size that
controls wheter WAL-skipped tables are finally WAL-logged or
fcync'ed. All of the TAP test performs WAL-logging so this adds an
item that performs file sync.
---
src/test/recovery/t/018_wal_optimize.pl | 38 ++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index b041121745..ba9185e2ba 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -11,7 +11,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 26;
+use Test::More tests => 32;
sub check_orphan_relfilenodes
{
@@ -43,6 +43,8 @@ sub run_wal_optimize
$node->append_conf('postgresql.conf', qq(
wal_level = $wal_level
max_prepared_transactions = 1
+wal_log_hints = on
+wal_skip_threshold = 0
));
$node->start;
@@ -102,7 +104,23 @@ max_prepared_transactions = 1
$result = $node->safe_psql('postgres', "SELECT count(*) FROM test2a;");
is($result, qq(1),
"wal_level = $wal_level, optimized truncation with prepared transaction");
+ # Same for file sync mode
+ # Tuples inserted after the truncation should be seen.
+ $node->safe_psql('postgres', "
+ SET wal_skip_threshold to 0;
+ BEGIN;
+ CREATE TABLE test2b (id serial PRIMARY KEY);
+ INSERT INTO test2b VALUES (DEFAULT);
+ TRUNCATE test2b;
+ INSERT INTO test2b VALUES (DEFAULT);
+ COMMIT;");
+
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->safe_psql('postgres', "SELECT count(*) FROM test2b;");
+ is($result, qq(1),
+ "wal_level = $wal_level, optimized truncation with file-sync");
# Data file for COPY query in follow-up tests.
my $basedir = $node->basedir;
@@ -178,6 +196,24 @@ max_prepared_transactions = 1
is($result, qq(3),
"wal_level = $wal_level, SET TABLESPACE in subtransaction");
+ $node->safe_psql('postgres', "
+ BEGIN;
+ CREATE TABLE test3a5 (c int PRIMARY KEY);
+ SAVEPOINT q; INSERT INTO test3a5 VALUES (1); ROLLBACK TO q;
+ CHECKPOINT;
+ INSERT INTO test3a5 VALUES (1); -- set index hint bit
+ INSERT INTO test3a5 VALUES (2);
+ COMMIT;");
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->psql('postgres', );
+ my($ret, $stdout, $stderr) = $node->psql(
+ 'postgres', "INSERT INTO test3a5 VALUES (2);");
+ is($ret, qq(3),
+ "wal_level = $wal_level, unique index LP_DEAD");
+ like($stderr, qr/violates unique/,
+ "wal_level = $wal_level, unique index LP_DEAD message");
+
# UPDATE touches two buffers; one is BufferNeedsWAL(); the other is not.
$node->safe_psql('postgres', "
BEGIN;
--
2.23.0
----Next_Part(Fri_Oct_25_13_12_51_2019_465)----
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v23 5/5] Additional test for new GUC setting.
@ 2019-08-28 05:12 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Kyotaro Horiguchi @ 2019-08-28 05:12 UTC (permalink / raw)
This patchset adds new GUC variable effective_io_block_size that
controls wheter WAL-skipped tables are finally WAL-logged or
fcync'ed. All of the TAP test performs WAL-logging so this adds an
item that performs file sync.
---
src/test/recovery/t/018_wal_optimize.pl | 38 ++++++++++++++++++++++++-
1 file changed, 37 insertions(+), 1 deletion(-)
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index ac62c77a42..470d4f048c 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -18,7 +18,7 @@ use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 26;
+use Test::More tests => 32;
# Make sure no orphan relfilenode files exist.
sub check_orphan_relfilenodes
@@ -52,6 +52,8 @@ sub run_wal_optimize
$node->append_conf('postgresql.conf', qq(
wal_level = $wal_level
max_prepared_transactions = 1
+wal_log_hints = on
+wal_skip_threshold = 0
));
$node->start;
@@ -111,7 +113,23 @@ max_prepared_transactions = 1
$result = $node->safe_psql('postgres', "SELECT count(*) FROM test2a;");
is($result, qq(1),
"wal_level = $wal_level, optimized truncation with prepared transaction");
+ # Same for file sync mode
+ # Tuples inserted after the truncation should be seen.
+ $node->safe_psql('postgres', "
+ SET wal_skip_threshold to 0;
+ BEGIN;
+ CREATE TABLE test2b (id serial PRIMARY KEY);
+ INSERT INTO test2b VALUES (DEFAULT);
+ TRUNCATE test2b;
+ INSERT INTO test2b VALUES (DEFAULT);
+ COMMIT;");
+
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->safe_psql('postgres', "SELECT count(*) FROM test2b;");
+ is($result, qq(1),
+ "wal_level = $wal_level, optimized truncation with file-sync");
# Data file for COPY query in follow-up tests.
my $basedir = $node->basedir;
@@ -187,6 +205,24 @@ max_prepared_transactions = 1
is($result, qq(3),
"wal_level = $wal_level, SET TABLESPACE in subtransaction");
+ $node->safe_psql('postgres', "
+ BEGIN;
+ CREATE TABLE test3a5 (c int PRIMARY KEY);
+ SAVEPOINT q; INSERT INTO test3a5 VALUES (1); ROLLBACK TO q;
+ CHECKPOINT;
+ INSERT INTO test3a5 VALUES (1); -- set index hint bit
+ INSERT INTO test3a5 VALUES (2);
+ COMMIT;");
+ $node->stop('immediate');
+ $node->start;
+ $result = $node->psql('postgres', );
+ my($ret, $stdout, $stderr) = $node->psql(
+ 'postgres', "INSERT INTO test3a5 VALUES (2);");
+ is($ret, qq(3),
+ "wal_level = $wal_level, unique index LP_DEAD");
+ like($stderr, qr/violates unique/,
+ "wal_level = $wal_level, unique index LP_DEAD message");
+
# UPDATE touches two buffers; one is BufferNeedsWAL(); the other is not.
$node->safe_psql('postgres', "
BEGIN;
--
2.23.0
----Next_Part(Wed_Nov__6_17_29_27_2019_575)----
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
@ 2023-12-06 10:42 Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
0 siblings, 1 reply; 28+ messages in thread
From: Alvaro Herrera @ 2023-12-06 10:42 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: jian he <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
On 2023-Dec-05, Amit Langote wrote:
> I've attempted to trim down the JSON_TABLE grammar (0004), but this is
> all I've managed so far. Among other things, I couldn't refactor the
> grammar to do away with the following:
>
> +%nonassoc NESTED
> +%left PATH
To recap, the reason we're arguing about this is that this creates two
new precedence classes, which are higher than everything else. Judging
by the discussios in thread [1], this is not acceptable. Without either
those new classes or the two hacks I describe below, the grammar has the
following shift/reduce conflict:
State 6220
2331 json_table_column_definition: NESTED . path_opt Sconst COLUMNS '(' json_table_column_definition_list ')'
2332 | NESTED . path_opt Sconst AS name COLUMNS '(' json_table_column_definition_list ')'
2636 unreserved_keyword: NESTED .
PATH shift, and go to state 6286
SCONST reduce using rule 2336 (path_opt)
PATH [reduce using rule 2636 (unreserved_keyword)]
$default reduce using rule 2636 (unreserved_keyword)
path_opt go to state 6287
First, while the grammar uses "NESTED path_opt" in the relevant productions, I
noticed that there's no test that uses NESTED without PATH, so if we break that
case, we won't notice. I propose we remove the PATH keyword from one of
the tests in jsonb_sqljson.sql in order to make sure the grammar
continues to work after whatever hacking we do:
diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out
index 7e8ae6a696..8fd2385cdc 100644
--- a/src/test/regress/expected/jsonb_sqljson.out
+++ b/src/test/regress/expected/jsonb_sqljson.out
@@ -1548,7 +1548,7 @@ HINT: JSON_TABLE column names must be distinct from one another.
SELECT * FROM JSON_TABLE(
jsonb 'null', '$[*]' AS p0
COLUMNS (
- NESTED PATH '$' AS p1 COLUMNS (
+ NESTED '$' AS p1 COLUMNS (
NESTED PATH '$' AS p11 COLUMNS ( foo int ),
NESTED PATH '$' AS p12 COLUMNS ( bar int )
),
diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql
index ea5db88b40..ea9b4ff8b6 100644
--- a/src/test/regress/sql/jsonb_sqljson.sql
+++ b/src/test/regress/sql/jsonb_sqljson.sql
@@ -617,7 +617,7 @@ SELECT * FROM JSON_TABLE(
SELECT * FROM JSON_TABLE(
jsonb 'null', '$[*]' AS p0
COLUMNS (
- NESTED PATH '$' AS p1 COLUMNS (
+ NESTED '$' AS p1 COLUMNS (
NESTED PATH '$' AS p11 COLUMNS ( foo int ),
NESTED PATH '$' AS p12 COLUMNS ( bar int )
),
Having done that, AFAICS there are two possible fixes for the grammar.
One is to keep the idea of assigning precedence explicitly to these
keywords, but do something less hackish -- we can put NESTED together
with UNBOUNDED, and classify PATH in the IDENT group. This requires no
further changes. This would make NESTED PATH follow the same rationale
as UNBOUNDED FOLLOWING / UNBOUNDED PRECEDING. Here's is a preliminary
patch for that (the large comment above needs to be updated.)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c15fcf2eb2..1493ac7580 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -887,9 +887,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
* json_predicate_type_constraint and json_key_uniqueness_constraint_opt
* productions (see comments there).
*/
-%nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */
+%nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
%nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
- SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT
+ SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
%left Op OPERATOR /* multi-character ops and user-defined operators */
%left '+' '-'
%left '*' '/' '%'
@@ -911,8 +911,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
*/
%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
-%nonassoc NESTED
-%left PATH
%%
/*
The other thing we can do is use the two-token lookahead trick, by
declaring
%token NESTED_LA
and using the parser.c code to replace NESTED with NESTED_LA when it is
followed by PATH. This doesn't require assigning precedence to
anything. We do need to expand the two rules that have "NESTED
opt_path Sconst" to each be two rules, one for "NESTED_LA PATH Sconst"
and another for "NESTED Sconst". So the opt_path production goes away.
This preliminary patch does that. (I did not touch the ecpg grammar, but
it needs an update too.)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index c15fcf2eb2..8e4c1d4ebe 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -817,7 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
* FORMAT_LA, NULLS_LA, WITH_LA, and WITHOUT_LA are needed to make the grammar
* LALR(1).
*/
-%token FORMAT_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
+%token FORMAT_LA NESTED_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
/*
* The grammar likewise thinks these tokens are keywords, but they are never
@@ -911,8 +911,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
*/
%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
-%nonassoc NESTED
-%left PATH
%%
/*
@@ -16771,7 +16769,7 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
- | NESTED path_opt Sconst
+ | NESTED_LA PATH Sconst
COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16783,7 +16781,19 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
- | NESTED path_opt Sconst AS name
+ | NESTED Sconst
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $2;
+ n->pathname = NULL;
+ n->columns = $5;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NESTED_LA PATH Sconst AS name
COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16795,6 +16805,19 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
+ | NESTED Sconst AS name
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $2;
+ n->pathname = $4;
+ n->columns = $7;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+
;
json_table_column_path_specification_clause_opt:
@@ -16802,11 +16825,6 @@ json_table_column_path_specification_clause_opt:
| /* EMPTY */ { $$ = NULL; }
;
-path_opt:
- PATH { }
- | /* EMPTY */ { }
- ;
-
json_table_plan_clause_opt:
PLAN '(' json_table_plan ')' { $$ = $3; }
| PLAN DEFAULT '(' json_table_default_plan_choices ')'
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index e17c310cc1..e3092f2c3e 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -138,6 +138,7 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
switch (cur_token)
{
case FORMAT:
+ case NESTED:
cur_token_length = 6;
break;
case NOT:
@@ -204,6 +205,16 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
}
break;
+ case NESTED:
+ /* Replace NESTED by NESTED_LA if it's followed by PATH */
+ switch (next_token)
+ {
+ case PATH:
+ cur_token = NESTED_LA;
+ break;
+ }
+ break;
+
case NOT:
/* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
switch (next_token)
I don't know which of the two "fixes" is less bad. Like Amit, I was not
able to find a solution to the problem by merely attaching precedences
to rules. (I did not try to mess with the precedence of
unreserved_keyword, because I'm pretty sure that would not be a good
solution even if I could make it work.)
[1] https://postgr.es/m/CADT4RqBPdbsZW7HS1jJP319TMRHs1hzUiP=iRJYR6UqgHCrgNQ@mail.gmail.com
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
@ 2023-12-06 14:02 ` Amit Langote <[email protected]>
2023-12-07 03:10 ` Re: remaining sql/json patches jian he <[email protected]>
2023-12-07 08:25 ` Re: remaining sql/json patches Peter Eisentraut <[email protected]>
0 siblings, 2 replies; 28+ messages in thread
From: Amit Langote @ 2023-12-06 14:02 UTC (permalink / raw)
To: Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
Thanks Alvaro.
On Wed, Dec 6, 2023 at 7:43 PM Alvaro Herrera <[email protected]> wrote:
> On 2023-Dec-05, Amit Langote wrote:
>
> > I've attempted to trim down the JSON_TABLE grammar (0004), but this is
> > all I've managed so far. Among other things, I couldn't refactor the
> > grammar to do away with the following:
> >
> > +%nonassoc NESTED
> > +%left PATH
>
> To recap, the reason we're arguing about this is that this creates two
> new precedence classes, which are higher than everything else. Judging
> by the discussios in thread [1], this is not acceptable. Without either
> those new classes or the two hacks I describe below, the grammar has the
> following shift/reduce conflict:
>
> State 6220
>
> 2331 json_table_column_definition: NESTED . path_opt Sconst COLUMNS '(' json_table_column_definition_list ')'
> 2332 | NESTED . path_opt Sconst AS name COLUMNS '(' json_table_column_definition_list ')'
> 2636 unreserved_keyword: NESTED .
>
> PATH shift, and go to state 6286
>
> SCONST reduce using rule 2336 (path_opt)
> PATH [reduce using rule 2636 (unreserved_keyword)]
> $default reduce using rule 2636 (unreserved_keyword)
>
> path_opt go to state 6287
>
>
>
> First, while the grammar uses "NESTED path_opt" in the relevant productions, I
> noticed that there's no test that uses NESTED without PATH, so if we break that
> case, we won't notice. I propose we remove the PATH keyword from one of
> the tests in jsonb_sqljson.sql in order to make sure the grammar
> continues to work after whatever hacking we do:
>
> diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out
> index 7e8ae6a696..8fd2385cdc 100644
> --- a/src/test/regress/expected/jsonb_sqljson.out
> +++ b/src/test/regress/expected/jsonb_sqljson.out
> @@ -1548,7 +1548,7 @@ HINT: JSON_TABLE column names must be distinct from one another.
> SELECT * FROM JSON_TABLE(
> jsonb 'null', '$[*]' AS p0
> COLUMNS (
> - NESTED PATH '$' AS p1 COLUMNS (
> + NESTED '$' AS p1 COLUMNS (
> NESTED PATH '$' AS p11 COLUMNS ( foo int ),
> NESTED PATH '$' AS p12 COLUMNS ( bar int )
> ),
> diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql
> index ea5db88b40..ea9b4ff8b6 100644
> --- a/src/test/regress/sql/jsonb_sqljson.sql
> +++ b/src/test/regress/sql/jsonb_sqljson.sql
> @@ -617,7 +617,7 @@ SELECT * FROM JSON_TABLE(
> SELECT * FROM JSON_TABLE(
> jsonb 'null', '$[*]' AS p0
> COLUMNS (
> - NESTED PATH '$' AS p1 COLUMNS (
> + NESTED '$' AS p1 COLUMNS (
> NESTED PATH '$' AS p11 COLUMNS ( foo int ),
> NESTED PATH '$' AS p12 COLUMNS ( bar int )
> ),
Fixed the test case like that in the attached.
> Having done that, AFAICS there are two possible fixes for the grammar.
> One is to keep the idea of assigning precedence explicitly to these
> keywords, but do something less hackish -- we can put NESTED together
> with UNBOUNDED, and classify PATH in the IDENT group. This requires no
> further changes. This would make NESTED PATH follow the same rationale
> as UNBOUNDED FOLLOWING / UNBOUNDED PRECEDING. Here's is a preliminary
> patch for that (the large comment above needs to be updated.)
>
> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
> index c15fcf2eb2..1493ac7580 100644
> --- a/src/backend/parser/gram.y
> +++ b/src/backend/parser/gram.y
> @@ -887,9 +887,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
> * json_predicate_type_constraint and json_key_uniqueness_constraint_opt
> * productions (see comments there).
> */
> -%nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */
> +%nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
> %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
> - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT
> + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
> %left Op OPERATOR /* multi-character ops and user-defined operators */
> %left '+' '-'
> %left '*' '/' '%'
> @@ -911,8 +911,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
> */
> %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
>
> -%nonassoc NESTED
> -%left PATH
> %%
>
> /*
>
>
> The other thing we can do is use the two-token lookahead trick, by
> declaring
> %token NESTED_LA
> and using the parser.c code to replace NESTED with NESTED_LA when it is
> followed by PATH. This doesn't require assigning precedence to
> anything. We do need to expand the two rules that have "NESTED
> opt_path Sconst" to each be two rules, one for "NESTED_LA PATH Sconst"
> and another for "NESTED Sconst". So the opt_path production goes away.
> This preliminary patch does that. (I did not touch the ecpg grammar, but
> it needs an update too.)
>
> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
> index c15fcf2eb2..8e4c1d4ebe 100644
> --- a/src/backend/parser/gram.y
> +++ b/src/backend/parser/gram.y
> @@ -817,7 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
> * FORMAT_LA, NULLS_LA, WITH_LA, and WITHOUT_LA are needed to make the grammar
> * LALR(1).
> */
> -%token FORMAT_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
> +%token FORMAT_LA NESTED_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
>
> /*
> * The grammar likewise thinks these tokens are keywords, but they are never
> @@ -911,8 +911,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
> */
> %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
>
> -%nonassoc NESTED
> -%left PATH
> %%
>
> /*
> @@ -16771,7 +16769,7 @@ json_table_column_definition:
> n->location = @1;
> $$ = (Node *) n;
> }
> - | NESTED path_opt Sconst
> + | NESTED_LA PATH Sconst
> COLUMNS '(' json_table_column_definition_list ')'
> {
> JsonTableColumn *n = makeNode(JsonTableColumn);
> @@ -16783,7 +16781,19 @@ json_table_column_definition:
> n->location = @1;
> $$ = (Node *) n;
> }
> - | NESTED path_opt Sconst AS name
> + | NESTED Sconst
> + COLUMNS '(' json_table_column_definition_list ')'
> + {
> + JsonTableColumn *n = makeNode(JsonTableColumn);
> +
> + n->coltype = JTC_NESTED;
> + n->pathspec = $2;
> + n->pathname = NULL;
> + n->columns = $5;
> + n->location = @1;
> + $$ = (Node *) n;
> + }
> + | NESTED_LA PATH Sconst AS name
> COLUMNS '(' json_table_column_definition_list ')'
> {
> JsonTableColumn *n = makeNode(JsonTableColumn);
> @@ -16795,6 +16805,19 @@ json_table_column_definition:
> n->location = @1;
> $$ = (Node *) n;
> }
> + | NESTED Sconst AS name
> + COLUMNS '(' json_table_column_definition_list ')'
> + {
> + JsonTableColumn *n = makeNode(JsonTableColumn);
> +
> + n->coltype = JTC_NESTED;
> + n->pathspec = $2;
> + n->pathname = $4;
> + n->columns = $7;
> + n->location = @1;
> + $$ = (Node *) n;
> + }
> +
> ;
>
> json_table_column_path_specification_clause_opt:
> @@ -16802,11 +16825,6 @@ json_table_column_path_specification_clause_opt:
> | /* EMPTY */ { $$ = NULL; }
> ;
>
> -path_opt:
> - PATH { }
> - | /* EMPTY */ { }
> - ;
> -
> json_table_plan_clause_opt:
> PLAN '(' json_table_plan ')' { $$ = $3; }
> | PLAN DEFAULT '(' json_table_default_plan_choices ')'
> diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
> index e17c310cc1..e3092f2c3e 100644
> --- a/src/backend/parser/parser.c
> +++ b/src/backend/parser/parser.c
> @@ -138,6 +138,7 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
> switch (cur_token)
> {
> case FORMAT:
> + case NESTED:
> cur_token_length = 6;
> break;
> case NOT:
> @@ -204,6 +205,16 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
> }
> break;
>
> + case NESTED:
> + /* Replace NESTED by NESTED_LA if it's followed by PATH */
> + switch (next_token)
> + {
> + case PATH:
> + cur_token = NESTED_LA;
> + break;
> + }
> + break;
> +
> case NOT:
> /* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
> switch (next_token)
>
>
> I don't know which of the two "fixes" is less bad.
I think I'm inclined toward adapting the LA-token fix (attached 0005),
because we've done that before with SQL/JSON constructors patch.
Also, if I understand the concerns that Tom mentioned at [1]
correctly, maybe we'd be better off not assigning precedence to
symbols as much as possible, so there's that too against the approach
#1.
Also I've attached 0006 to add news tests under ECPG for the SQL/JSON
query functions, which I haven't done so far but realized after you
mentioned ECPG. It also includes the ECPG variant of the LA-token
fix. I'll eventually merge it into 0003 and 0004 after expanding the
test cases some more. I do wonder what kinds of tests we normally add
to ECPG suite but not others?
Finally, I also fixed a couple of silly mistakes in 0003 around
transformJsonBehavior() and some further assorted tightening in the ON
ERROR / EMPTY expression coercion handling code.
--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
[application/octet-stream] v28-0005-JSON_TABLE-don-t-assign-precedence-to-NESTED-PAT.patch (3.5K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/2-v28-0005-JSON_TABLE-don-t-assign-precedence-to-NESTED-PAT.patch)
download | inline diff:
From 6fb262ad6fa24f481692c62d0e90940ab44bbd96 Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Wed, 6 Dec 2023 22:29:13 +0900
Subject: [PATCH v28 5/6] JSON_TABLE: don't assign precedence to NESTED, PATH
---
src/backend/parser/gram.y | 37 +++++++++++++++++++++++++++----------
src/backend/parser/parser.c | 11 +++++++++++
2 files changed, 38 insertions(+), 10 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3755434af0..9bed5168e3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -817,7 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
* FORMAT_LA, NULLS_LA, WITH_LA, and WITHOUT_LA are needed to make the grammar
* LALR(1).
*/
-%token FORMAT_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
+%token FORMAT_LA NESTED_LA NOT_LA NULLS_LA WITH_LA WITHOUT_LA
/*
* The grammar likewise thinks these tokens are keywords, but they are never
@@ -911,8 +911,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
*/
%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
-%nonassoc NESTED
-%left PATH
%%
/*
@@ -16771,7 +16769,7 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
- | NESTED path_opt Sconst
+ | NESTED_LA PATH Sconst
COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16783,7 +16781,7 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
- | NESTED path_opt Sconst AS name
+ | NESTED_LA PATH Sconst AS name
COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16795,6 +16793,30 @@ json_table_column_definition:
n->location = @1;
$$ = (Node *) n;
}
+ | NESTED Sconst
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $2;
+ n->pathname = NULL;
+ n->columns = $5;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NESTED Sconst AS name
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $2;
+ n->pathname = $4;
+ n->columns = $7;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
json_table_column_path_specification_clause_opt:
@@ -16802,11 +16824,6 @@ json_table_column_path_specification_clause_opt:
| /* EMPTY */ { $$ = NULL; }
;
-path_opt:
- PATH { }
- | /* EMPTY */ { }
- ;
-
json_table_plan_clause_opt:
PLAN '(' json_table_plan ')' { $$ = $3; }
| PLAN DEFAULT '(' json_table_default_plan_choices ')'
diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c
index e17c310cc1..e3092f2c3e 100644
--- a/src/backend/parser/parser.c
+++ b/src/backend/parser/parser.c
@@ -138,6 +138,7 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
switch (cur_token)
{
case FORMAT:
+ case NESTED:
cur_token_length = 6;
break;
case NOT:
@@ -204,6 +205,16 @@ base_yylex(YYSTYPE *lvalp, YYLTYPE *llocp, core_yyscan_t yyscanner)
}
break;
+ case NESTED:
+ /* Replace NESTED by NESTED_LA if it's followed by PATH */
+ switch (next_token)
+ {
+ case PATH:
+ cur_token = NESTED_LA;
+ break;
+ }
+ break;
+
case NOT:
/* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
switch (next_token)
--
2.35.3
[application/octet-stream] v28-0002-Add-soft-error-handling-to-populate_record_field.patch (23.2K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/3-v28-0002-Add-soft-error-handling-to-populate_record_field.patch)
download | inline diff:
From ee36f9d8bd4ad1f2820d5b5b985be79448676cb3 Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Wed, 22 Nov 2023 13:18:49 +0900
Subject: [PATCH v28 2/6] Add soft error handling to populate_record_field()
An uncoming patch would like the ability to call it from the
executor for some SQL/JSON expression nodes and ask to suppress any
errors that may occur.
This commit does two things mainly:
* It modifies the various interfaces internal to jsonfuncs.c to pass
the ErrorSaveContext around.
* Make necessary modifications to handle the cases where the
processing is aborted partway through various functions that take
an ErrorSaveContext when a soft error occurs.
Note that the above changes are only intended to suppress errors in
the functions in jsonfuncs.c, but not those in any external functions
that the functions in jsonfuncs.c in turn call, such as those from
arrayfuncs.c. It is assumed that the various populate_* functions
validate the data before passing those to external functions.
Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com
---
src/backend/utils/adt/jsonfuncs.c | 280 ++++++++++++++++++++++--------
1 file changed, 210 insertions(+), 70 deletions(-)
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index aa37c401e5..1574ed5985 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -265,6 +265,7 @@ typedef struct PopulateArrayContext
int *dims; /* dimensions */
int *sizes; /* current dimension counters */
int ndims; /* number of dimensions */
+ Node *escontext; /* For soft-error handling */
} PopulateArrayContext;
/* state for populate_array_json() */
@@ -389,7 +390,8 @@ static JsonParseErrorType elements_array_element_end(void *state, bool isnull);
static JsonParseErrorType elements_scalar(void *state, char *token, JsonTokenType tokentype);
/* turn a json object into a hash table */
-static HTAB *get_json_object_as_hash(char *json, int len, const char *funcname);
+static HTAB *get_json_object_as_hash(char *json, int len, const char *funcname,
+ Node *escontext);
/* semantic actions for populate_array_json */
static JsonParseErrorType populate_array_object_start(void *_state);
@@ -431,37 +433,42 @@ static Datum populate_record_worker(FunctionCallInfo fcinfo, const char *funcnam
/* helper functions for populate_record[set] */
static HeapTupleHeader populate_record(TupleDesc tupdesc, RecordIOData **record_p,
HeapTupleHeader defaultval, MemoryContext mcxt,
- JsObject *obj);
+ JsObject *obj, Node *escontext);
static void get_record_type_from_argument(FunctionCallInfo fcinfo,
const char *funcname,
PopulateRecordCache *cache);
static void get_record_type_from_query(FunctionCallInfo fcinfo,
const char *funcname,
PopulateRecordCache *cache);
-static void JsValueToJsObject(JsValue *jsv, JsObject *jso);
+static bool JsValueToJsObject(JsValue *jsv, JsObject *jso, Node *escontext);
static Datum populate_composite(CompositeIOData *io, Oid typid,
const char *colname, MemoryContext mcxt,
- HeapTupleHeader defaultval, JsValue *jsv, bool isnull);
-static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv);
+ HeapTupleHeader defaultval, JsValue *jsv, bool *isnull,
+ Node *escontext);
+static Datum populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
+ bool *isnull, Node *escontext);
static void prepare_column_cache(ColumnIOData *column, Oid typid, int32 typmod,
MemoryContext mcxt, bool need_scalar);
static Datum populate_record_field(ColumnIOData *col, Oid typid, int32 typmod,
const char *colname, MemoryContext mcxt, Datum defaultval,
- JsValue *jsv, bool *isnull);
+ JsValue *jsv, bool *isnull, Node *escontext);
static RecordIOData *allocate_record_info(MemoryContext mcxt, int ncolumns);
static bool JsObjectGetField(JsObject *obj, char *field, JsValue *jsv);
static void populate_recordset_record(PopulateRecordsetState *state, JsObject *obj);
-static void populate_array_json(PopulateArrayContext *ctx, char *json, int len);
-static void populate_array_dim_jsonb(PopulateArrayContext *ctx, JsonbValue *jbv,
+static bool populate_array_json(PopulateArrayContext *ctx, char *json, int len);
+static bool populate_array_dim_jsonb(PopulateArrayContext *ctx, JsonbValue *jbv,
int ndim);
static void populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim);
-static void populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims);
-static void populate_array_check_dimension(PopulateArrayContext *ctx, int ndim);
-static void populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv);
+static bool populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims);
+static bool populate_array_check_dimension(PopulateArrayContext *ctx, int ndim);
+static bool populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv);
static Datum populate_array(ArrayIOData *aio, const char *colname,
- MemoryContext mcxt, JsValue *jsv);
+ MemoryContext mcxt, JsValue *jsv,
+ bool *isnull,
+ Node *escontext);
static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname,
- MemoryContext mcxt, JsValue *jsv, bool isnull);
+ MemoryContext mcxt, JsValue *jsv, bool isnull,
+ Node *escontext);
/* functions supporting jsonb_delete, jsonb_set and jsonb_concat */
static JsonbValue *IteratorConcat(JsonbIterator **it1, JsonbIterator **it2,
@@ -2484,12 +2491,12 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim)
if (ndim <= 0)
{
if (ctx->colname)
- ereport(ERROR,
+ errsave(ctx->escontext,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("expected JSON array"),
errhint("See the value of key \"%s\".", ctx->colname)));
else
- ereport(ERROR,
+ errsave(ctx->escontext,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("expected JSON array")));
}
@@ -2506,13 +2513,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim)
appendStringInfo(&indices, "[%d]", ctx->sizes[i]);
if (ctx->colname)
- ereport(ERROR,
+ errsave(ctx->escontext,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("expected JSON array"),
errhint("See the array element %s of key \"%s\".",
indices.data, ctx->colname)));
else
- ereport(ERROR,
+ errsave(ctx->escontext,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("expected JSON array"),
errhint("See the array element %s.",
@@ -2520,8 +2527,13 @@ populate_array_report_expected_array(PopulateArrayContext *ctx, int ndim)
}
}
-/* set the number of dimensions of the populated array when it becomes known */
-static void
+/*
+ * Validate and set ndims for populating an array with some
+ * populate_array_*() function.
+ *
+ * Returns false if the input (ndims) is erratic.
+ */
+static bool
populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims)
{
int i;
@@ -2529,7 +2541,12 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims)
Assert(ctx->ndims <= 0);
if (ndims <= 0)
+ {
populate_array_report_expected_array(ctx, ndims);
+ /* Getting here means the error was reported softly. */
+ Assert(SOFT_ERROR_OCCURRED(ctx->escontext));
+ return false;
+ }
ctx->ndims = ndims;
ctx->dims = palloc(sizeof(int) * ndims);
@@ -2537,10 +2554,16 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims)
for (i = 0; i < ndims; i++)
ctx->dims[i] = -1; /* dimensions are unknown yet */
+
+ return true;
}
-/* check the populated subarray dimension */
-static void
+/*
+ * Check the populated subarray dimension
+ *
+ * Returns false if the input (ndims) is erratic.
+ */
+static bool
populate_array_check_dimension(PopulateArrayContext *ctx, int ndim)
{
int dim = ctx->sizes[ndim]; /* current dimension counter */
@@ -2548,21 +2571,31 @@ populate_array_check_dimension(PopulateArrayContext *ctx, int ndim)
if (ctx->dims[ndim] == -1)
ctx->dims[ndim] = dim; /* assign dimension if not yet known */
else if (ctx->dims[ndim] != dim)
- ereport(ERROR,
+ errsave(ctx->escontext,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("malformed JSON array"),
errdetail("Multidimensional arrays must have "
"sub-arrays with matching dimensions.")));
+ /* Nothing to do on an error. */
+ if (SOFT_ERROR_OCCURRED(ctx->escontext))
+ return false;
+
/* reset the current array dimension size counter */
ctx->sizes[ndim] = 0;
/* increment the parent dimension counter if it is a nested sub-array */
if (ndim > 0)
ctx->sizes[ndim - 1]++;
+
+ return true;
}
-static void
+/*
+ * Returns true if the array element value was successfully extracted from jsv
+ * and added to ctx->astate. False if an error occurred when doing so.
+ */
+static bool
populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv)
{
Datum element;
@@ -2573,13 +2606,18 @@ populate_array_element(PopulateArrayContext *ctx, int ndim, JsValue *jsv)
ctx->aio->element_type,
ctx->aio->element_typmod,
NULL, ctx->mcxt, PointerGetDatum(NULL),
- jsv, &element_isnull);
+ jsv, &element_isnull, ctx->escontext);
+ /* Nothing to do on an error. */
+ if (SOFT_ERROR_OCCURRED(ctx->escontext))
+ return false;
accumArrayResult(ctx->astate, element, element_isnull,
ctx->aio->element_type, ctx->acxt);
Assert(ndim > 0);
ctx->sizes[ndim - 1]++; /* increment current dimension counter */
+
+ return true;
}
/* json object start handler for populate_array_json() */
@@ -2592,7 +2630,12 @@ populate_array_object_start(void *_state)
if (state->ctx->ndims <= 0)
populate_array_assign_ndims(state->ctx, ndim);
else if (ndim < state->ctx->ndims)
+ {
populate_array_report_expected_array(state->ctx, ndim);
+ /* Getting here means the error was reported softly. */
+ Assert(SOFT_ERROR_OCCURRED(state->ctx->escontext));
+ return JSON_SEM_ACTION_FAILED;
+ }
return JSON_SUCCESS;
}
@@ -2609,7 +2652,11 @@ populate_array_array_end(void *_state)
populate_array_assign_ndims(ctx, ndim + 1);
if (ndim < ctx->ndims)
- populate_array_check_dimension(ctx, ndim);
+ {
+ /* Report if an error occurred. */
+ if (!populate_array_check_dimension(ctx, ndim))
+ return JSON_SEM_ACTION_FAILED;
+ }
return JSON_SUCCESS;
}
@@ -2667,7 +2714,9 @@ populate_array_element_end(void *_state, bool isnull)
state->element_start) * sizeof(char);
}
- populate_array_element(ctx, ndim, &jsv);
+ /* Report if an error occurred. */
+ if (!populate_array_element(ctx, ndim, &jsv))
+ return JSON_SEM_ACTION_FAILED;
}
return JSON_SUCCESS;
@@ -2684,7 +2733,12 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype)
if (ctx->ndims <= 0)
populate_array_assign_ndims(ctx, ndim);
else if (ndim < ctx->ndims)
+ {
populate_array_report_expected_array(ctx, ndim);
+ /* Getting here means the error was reported softly. */
+ Assert(SOFT_ERROR_OCCURRED(state->ctx->escontext));
+ return JSON_SEM_ACTION_FAILED;
+ }
if (ndim == ctx->ndims)
{
@@ -2697,8 +2751,12 @@ populate_array_scalar(void *_state, char *token, JsonTokenType tokentype)
return JSON_SUCCESS;
}
-/* parse a json array and populate array */
-static void
+/*
+ * Parse a json array and populate array
+ *
+ * Returns false if an error occurs when parsing.
+ */
+static bool
populate_array_json(PopulateArrayContext *ctx, char *json, int len)
{
PopulateArrayState state;
@@ -2716,19 +2774,25 @@ populate_array_json(PopulateArrayContext *ctx, char *json, int len)
sem.array_element_end = populate_array_element_end;
sem.scalar = populate_array_scalar;
- pg_parse_json_or_ereport(state.lex, &sem);
-
- /* number of dimensions should be already known */
- Assert(ctx->ndims > 0 && ctx->dims);
+ if (pg_parse_json_or_errsave(state.lex, &sem, ctx->escontext))
+ {
+ /* number of dimensions should be already known */
+ Assert(ctx->ndims > 0 && ctx->dims);
+ }
freeJsonLexContext(state.lex);
+
+ return !SOFT_ERROR_OCCURRED(ctx->escontext);
}
/*
* populate_array_dim_jsonb() -- Iterate recursively through jsonb sub-array
* elements and accumulate result using given ArrayBuildState.
+ *
+ * Returns false if we return partway through because of an error in a
+ * subroutine.
*/
-static void
+static bool
populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
JsonbValue *jbv, /* jsonb sub-array */
int ndim) /* current dimension */
@@ -2742,7 +2806,12 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
check_stack_depth();
if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc))
+ {
populate_array_report_expected_array(ctx, ndim - 1);
+ /* Getting here means the error was reported softly. */
+ Assert(SOFT_ERROR_OCCURRED(ctx->escontext));
+ return false;
+ }
Assert(!JsonContainerIsScalar(jbc));
@@ -2763,7 +2832,10 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
(tok == WJB_ELEM &&
(val.type != jbvBinary ||
!JsonContainerIsArray(val.val.binary.data)))))
- populate_array_assign_ndims(ctx, ndim);
+ {
+ if (!populate_array_assign_ndims(ctx, ndim))
+ return false;
+ }
jsv.is_json = false;
jsv.val.jsonb = &val;
@@ -2776,16 +2848,21 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
* it is not the innermost dimension.
*/
if (ctx->ndims > 0 && ndim >= ctx->ndims)
- populate_array_element(ctx, ndim, &jsv);
+ {
+ if (!populate_array_element(ctx, ndim, &jsv))
+ return false;
+ }
else
{
/* populate child sub-array */
- populate_array_dim_jsonb(ctx, &val, ndim + 1);
+ if (!populate_array_dim_jsonb(ctx, &val, ndim + 1))
+ return false;
/* number of dimensions should be already known */
Assert(ctx->ndims > 0 && ctx->dims);
- populate_array_check_dimension(ctx, ndim);
+ if (!populate_array_check_dimension(ctx, ndim))
+ return false;
}
tok = JsonbIteratorNext(&it, &val, true);
@@ -2796,14 +2873,22 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
/* free iterator, iterating until WJB_DONE */
tok = JsonbIteratorNext(&it, &val, true);
Assert(tok == WJB_DONE && !it);
+
+ return true;
}
-/* recursively populate an array from json/jsonb */
+/*
+ * Recursively populate an array from json/jsonb
+ *
+ * *isnull is set to true if an error is reported during parsing.
+ */
static Datum
populate_array(ArrayIOData *aio,
const char *colname,
MemoryContext mcxt,
- JsValue *jsv)
+ JsValue *jsv,
+ bool *isnull,
+ Node *escontext)
{
PopulateArrayContext ctx;
Datum result;
@@ -2818,14 +2903,27 @@ populate_array(ArrayIOData *aio,
ctx.ndims = 0; /* unknown yet */
ctx.dims = NULL;
ctx.sizes = NULL;
+ ctx.escontext = escontext;
if (jsv->is_json)
- populate_array_json(&ctx, jsv->val.json.str,
- jsv->val.json.len >= 0 ? jsv->val.json.len
- : strlen(jsv->val.json.str));
+ {
+ /* Return null if an error was found. */
+ if (!populate_array_json(&ctx, jsv->val.json.str,
+ jsv->val.json.len >= 0 ? jsv->val.json.len
+ : strlen(jsv->val.json.str)))
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
+ }
else
{
- populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1);
+ /* Return null if an error was found. */
+ if (!populate_array_dim_jsonb(&ctx, jsv->val.jsonb, 1))
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
ctx.dims[0] = ctx.sizes[0];
}
@@ -2843,11 +2941,16 @@ populate_array(ArrayIOData *aio,
pfree(ctx.sizes);
pfree(lbs);
+ *isnull = false;
return result;
}
-static void
-JsValueToJsObject(JsValue *jsv, JsObject *jso)
+/*
+ * Returns false if an error occurs, provided escontext points to an
+ * ErrorSaveContext.
+ */
+static bool
+JsValueToJsObject(JsValue *jsv, JsObject *jso, Node *escontext)
{
jso->is_json = jsv->is_json;
@@ -2859,7 +2962,8 @@ JsValueToJsObject(JsValue *jsv, JsObject *jso)
jsv->val.json.len >= 0
? jsv->val.json.len
: strlen(jsv->val.json.str),
- "populate_composite");
+ "populate_composite",
+ escontext);
}
else
{
@@ -2877,7 +2981,7 @@ JsValueToJsObject(JsValue *jsv, JsObject *jso)
is_scalar = IsAJsonbScalar(jbv) ||
(jbv->type == jbvBinary &&
JsonContainerIsScalar(jbv->val.binary.data));
- ereport(ERROR,
+ errsave(escontext,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
is_scalar
? errmsg("cannot call %s on a scalar",
@@ -2886,6 +2990,8 @@ JsValueToJsObject(JsValue *jsv, JsObject *jso)
"populate_composite")));
}
}
+
+ return !SOFT_ERROR_OCCURRED(escontext);
}
/* acquire or update cached tuple descriptor for a composite type */
@@ -2912,7 +3018,12 @@ update_cached_tupdesc(CompositeIOData *io, MemoryContext mcxt)
}
}
-/* recursively populate a composite (row type) value from json/jsonb */
+/*
+ * Recursively populate a composite (row type) value from json/jsonb
+ *
+ * Returns null if an error occurs in a subroutine, provided escontext points
+ * to an ErrorSaveContext.
+ */
static Datum
populate_composite(CompositeIOData *io,
Oid typid,
@@ -2920,14 +3031,15 @@ populate_composite(CompositeIOData *io,
MemoryContext mcxt,
HeapTupleHeader defaultval,
JsValue *jsv,
- bool isnull)
+ bool *isnull,
+ Node *escontext)
{
Datum result;
/* acquire/update cached tuple descriptor */
update_cached_tupdesc(io, mcxt);
- if (isnull)
+ if (*isnull)
result = (Datum) 0;
else
{
@@ -2935,11 +3047,15 @@ populate_composite(CompositeIOData *io,
JsObject jso;
/* prepare input value */
- JsValueToJsObject(jsv, &jso);
+ if (!JsValueToJsObject(jsv, &jso, escontext))
+ {
+ *isnull = true;
+ return (Datum) 0;
+ }
/* populate resulting record tuple */
tuple = populate_record(io->tupdesc, &io->record_io,
- defaultval, mcxt, &jso);
+ defaultval, mcxt, &jso, escontext);
result = HeapTupleHeaderGetDatum(tuple);
JsObjectFree(&jso);
@@ -2951,14 +3067,20 @@ populate_composite(CompositeIOData *io,
* now, we can tell by comparing typid to base_typid.)
*/
if (typid != io->base_typid && typid != RECORDOID)
- domain_check(result, isnull, typid, &io->domain_info, mcxt);
+ domain_check(result, *isnull, typid, &io->domain_info, mcxt);
return result;
}
-/* populate non-null scalar value from json/jsonb value */
+/*
+ * Populate non-null scalar value from json/jsonb value.
+ *
+ * Returns null if an error occurs during the call to type input function,
+ * provided escontext is valid.
+ */
static Datum
-populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv)
+populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
+ bool *isnull, Node *escontext)
{
Datum res;
char *str = NULL;
@@ -3029,7 +3151,12 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv)
elog(ERROR, "unrecognized jsonb type: %d", (int) jbv->type);
}
- res = InputFunctionCall(&io->typiofunc, str, io->typioparam, typmod);
+ if (!InputFunctionCallSafe(&io->typiofunc, str, io->typioparam, typmod,
+ escontext, &res))
+ {
+ res = (Datum) 0;
+ *isnull = true;
+ }
/* free temporary buffer */
if (str != json)
@@ -3044,7 +3171,8 @@ populate_domain(DomainIOData *io,
const char *colname,
MemoryContext mcxt,
JsValue *jsv,
- bool isnull)
+ bool isnull,
+ Node *escontext)
{
Datum res;
@@ -3055,8 +3183,8 @@ populate_domain(DomainIOData *io,
res = populate_record_field(io->base_io,
io->base_typid, io->base_typmod,
colname, mcxt, PointerGetDatum(NULL),
- jsv, &isnull);
- Assert(!isnull);
+ jsv, &isnull, escontext);
+ Assert(!isnull || SOFT_ERROR_OCCURRED(escontext));
}
domain_check(res, isnull, typid, &io->domain_info, mcxt);
@@ -3160,7 +3288,8 @@ populate_record_field(ColumnIOData *col,
MemoryContext mcxt,
Datum defaultval,
JsValue *jsv,
- bool *isnull)
+ bool *isnull,
+ Node *escontext)
{
TypeCat typcat;
@@ -3193,10 +3322,12 @@ populate_record_field(ColumnIOData *col,
switch (typcat)
{
case TYPECAT_SCALAR:
- return populate_scalar(&col->scalar_io, typid, typmod, jsv);
+ return populate_scalar(&col->scalar_io, typid, typmod, jsv,
+ isnull, escontext);
case TYPECAT_ARRAY:
- return populate_array(&col->io.array, colname, mcxt, jsv);
+ return populate_array(&col->io.array, colname, mcxt, jsv,
+ isnull, escontext);
case TYPECAT_COMPOSITE:
case TYPECAT_COMPOSITE_DOMAIN:
@@ -3205,11 +3336,12 @@ populate_record_field(ColumnIOData *col,
DatumGetPointer(defaultval)
? DatumGetHeapTupleHeader(defaultval)
: NULL,
- jsv, *isnull);
+ jsv, isnull,
+ escontext);
case TYPECAT_DOMAIN:
return populate_domain(&col->io.domain, typid, colname, mcxt,
- jsv, *isnull);
+ jsv, *isnull, escontext);
default:
elog(ERROR, "unrecognized type category '%c'", typcat);
@@ -3266,7 +3398,8 @@ populate_record(TupleDesc tupdesc,
RecordIOData **record_p,
HeapTupleHeader defaultval,
MemoryContext mcxt,
- JsObject *obj)
+ JsObject *obj,
+ Node *escontext)
{
RecordIOData *record = *record_p;
Datum *values;
@@ -3358,7 +3491,8 @@ populate_record(TupleDesc tupdesc,
mcxt,
nulls[i] ? (Datum) 0 : values[i],
&field,
- &nulls[i]);
+ &nulls[i],
+ escontext);
}
res = heap_form_tuple(tupdesc, values, nulls);
@@ -3445,6 +3579,7 @@ populate_record_worker(FunctionCallInfo fcinfo, const char *funcname,
JsValue jsv = {0};
HeapTupleHeader rec;
Datum rettuple;
+ bool isnull;
JsonbValue jbv;
MemoryContext fnmcxt = fcinfo->flinfo->fn_mcxt;
PopulateRecordCache *cache = fcinfo->flinfo->fn_extra;
@@ -3531,8 +3666,11 @@ populate_record_worker(FunctionCallInfo fcinfo, const char *funcname,
jbv.val.binary.len = VARSIZE(jb) - VARHDRSZ;
}
+ isnull = false;
rettuple = populate_composite(&cache->c.io.composite, cache->argtype,
- NULL, fnmcxt, rec, &jsv, false);
+ NULL, fnmcxt, rec, &jsv, &isnull,
+ NULL);
+ Assert(!isnull);
PG_RETURN_DATUM(rettuple);
}
@@ -3543,7 +3681,8 @@ populate_record_worker(FunctionCallInfo fcinfo, const char *funcname,
* decompose a json object into a hash table.
*/
static HTAB *
-get_json_object_as_hash(char *json, int len, const char *funcname)
+get_json_object_as_hash(char *json, int len, const char *funcname,
+ Node *escontext)
{
HASHCTL ctl;
HTAB *tab;
@@ -3572,7 +3711,7 @@ get_json_object_as_hash(char *json, int len, const char *funcname)
sem->object_field_start = hash_object_field_start;
sem->object_field_end = hash_object_field_end;
- pg_parse_json_or_ereport(state->lex, sem);
+ pg_parse_json_or_errsave(state->lex, sem, escontext);
freeJsonLexContext(state->lex);
@@ -3743,7 +3882,8 @@ populate_recordset_record(PopulateRecordsetState *state, JsObject *obj)
&cache->c.io.composite.record_io,
state->rec,
cache->fn_mcxt,
- obj);
+ obj,
+ NULL);
/* if it's domain over composite, check domain constraints */
if (cache->c.typcat == TYPECAT_COMPOSITE_DOMAIN)
--
2.35.3
[application/octet-stream] v28-0006-ECPG-add-SQL-JSON-query-function-tests-and-parse.patch (12.8K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/4-v28-0006-ECPG-add-SQL-JSON-query-function-tests-and-parse.patch)
download | inline diff:
From 4d3b42bcc4823361163e773807073db3bdee3e3c Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Wed, 6 Dec 2023 22:33:09 +0900
Subject: [PATCH v28 6/6] ECPG: add SQL/JSON query function tests and parser LA
fix for NESTED
---
src/interfaces/ecpg/preproc/parse.pl | 1 +
src/interfaces/ecpg/preproc/parser.c | 11 ++
src/interfaces/ecpg/test/ecpg_schedule | 1 +
.../test/expected/sql-sqljson_queryfuncs.c | 171 ++++++++++++++++++
.../expected/sql-sqljson_queryfuncs.stderr | 46 +++++
.../expected/sql-sqljson_queryfuncs.stdout | 3 +
src/interfaces/ecpg/test/sql/Makefile | 1 +
src/interfaces/ecpg/test/sql/meson.build | 1 +
.../ecpg/test/sql/sqljson_queryfuncs.pgc | 45 +++++
9 files changed, 280 insertions(+)
create mode 100644 src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.c
create mode 100644 src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stderr
create mode 100644 src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stdout
create mode 100644 src/interfaces/ecpg/test/sql/sqljson_queryfuncs.pgc
diff --git a/src/interfaces/ecpg/preproc/parse.pl b/src/interfaces/ecpg/preproc/parse.pl
index 7574fc3110..d27fc7c87d 100644
--- a/src/interfaces/ecpg/preproc/parse.pl
+++ b/src/interfaces/ecpg/preproc/parse.pl
@@ -56,6 +56,7 @@ my %replace_token = (
# or in the block
my %replace_string = (
'FORMAT_LA' => 'format',
+ 'NESTED_LA' => 'nested',
'NOT_LA' => 'not',
'NULLS_LA' => 'nulls',
'WITH_LA' => 'with',
diff --git a/src/interfaces/ecpg/preproc/parser.c b/src/interfaces/ecpg/preproc/parser.c
index 38e7acb680..47172fb780 100644
--- a/src/interfaces/ecpg/preproc/parser.c
+++ b/src/interfaces/ecpg/preproc/parser.c
@@ -79,6 +79,7 @@ filtered_base_yylex(void)
switch (cur_token)
{
case FORMAT:
+ case NESTED:
case NOT:
case NULLS_P:
case WITH:
@@ -122,6 +123,16 @@ filtered_base_yylex(void)
}
break;
+ case NESTED:
+ /* Replace NESTED by NESTED_LA if it's followed by PATH */
+ switch (next_token)
+ {
+ case PATH:
+ cur_token = NESTED_LA;
+ break;
+ }
+ break;
+
case NOT:
/* Replace NOT by NOT_LA if it's followed by BETWEEN, IN, etc */
switch (next_token)
diff --git a/src/interfaces/ecpg/test/ecpg_schedule b/src/interfaces/ecpg/test/ecpg_schedule
index 39814a39c1..770a1411f3 100644
--- a/src/interfaces/ecpg/test/ecpg_schedule
+++ b/src/interfaces/ecpg/test/ecpg_schedule
@@ -51,6 +51,7 @@ test: sql/oldexec
test: sql/quote
test: sql/show
test: sql/sqljson
+test: sql/sqljson_queryfuncs
test: sql/insupd
test: sql/parser
test: sql/prepareas
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.c b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.c
new file mode 100644
index 0000000000..59ed7fb699
--- /dev/null
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.c
@@ -0,0 +1,171 @@
+/* Processed by ecpg (regression mode) */
+/* These include files are added by the preprocessor */
+#include <ecpglib.h>
+#include <ecpgerrno.h>
+#include <sqlca.h>
+/* End of automatic include section */
+#define ECPGdebug(X,Y) ECPGdebug((X)+100,(Y))
+
+#line 1 "sqljson_queryfuncs.pgc"
+#include <stdio.h>
+
+
+#line 1 "sqlca.h"
+#ifndef POSTGRES_SQLCA_H
+#define POSTGRES_SQLCA_H
+
+#ifndef PGDLLIMPORT
+#if defined(WIN32) || defined(__CYGWIN__)
+#define PGDLLIMPORT __declspec (dllimport)
+#else
+#define PGDLLIMPORT
+#endif /* __CYGWIN__ */
+#endif /* PGDLLIMPORT */
+
+#define SQLERRMC_LEN 150
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+struct sqlca_t
+{
+ char sqlcaid[8];
+ long sqlabc;
+ long sqlcode;
+ struct
+ {
+ int sqlerrml;
+ char sqlerrmc[SQLERRMC_LEN];
+ } sqlerrm;
+ char sqlerrp[8];
+ long sqlerrd[6];
+ /* Element 0: empty */
+ /* 1: OID of processed tuple if applicable */
+ /* 2: number of rows processed */
+ /* after an INSERT, UPDATE or */
+ /* DELETE statement */
+ /* 3: empty */
+ /* 4: empty */
+ /* 5: empty */
+ char sqlwarn[8];
+ /* Element 0: set to 'W' if at least one other is 'W' */
+ /* 1: if 'W' at least one character string */
+ /* value was truncated when it was */
+ /* stored into a host variable. */
+
+ /*
+ * 2: if 'W' a (hopefully) non-fatal notice occurred
+ */ /* 3: empty */
+ /* 4: empty */
+ /* 5: empty */
+ /* 6: empty */
+ /* 7: empty */
+
+ char sqlstate[5];
+};
+
+struct sqlca_t *ECPGget_sqlca(void);
+
+#ifndef POSTGRES_ECPG_INTERNAL
+#define sqlca (*ECPGget_sqlca())
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
+
+#line 3 "sqljson_queryfuncs.pgc"
+
+
+#line 1 "regression.h"
+
+
+
+
+
+
+#line 4 "sqljson_queryfuncs.pgc"
+
+
+/* exec sql whenever sqlerror sqlprint ; */
+#line 6 "sqljson_queryfuncs.pgc"
+
+
+int
+main ()
+{
+/* exec sql begin declare section */
+
+
+#line 12 "sqljson_queryfuncs.pgc"
+ char json [ 1024 ] ;
+/* exec sql end declare section */
+#line 13 "sqljson_queryfuncs.pgc"
+
+
+ ECPGdebug (1, stderr);
+
+ { ECPGconnect(__LINE__, 0, "ecpg1_regression" , NULL, NULL , NULL, 0);
+#line 17 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 17 "sqljson_queryfuncs.pgc"
+
+ { ECPGsetcommit(__LINE__, "on", NULL);
+#line 18 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 18 "sqljson_queryfuncs.pgc"
+
+
+ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_exists ( jsonb '[]' , '$' )", ECPGt_EOIT,
+ ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char),
+ ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT);
+#line 20 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 20 "sqljson_queryfuncs.pgc"
+
+ printf("Found json=%s\n", json);
+
+ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_value ( jsonb '{\"a\": 123}' , '$' || '.' || 'b' default 'foo' on empty )", ECPGt_EOIT,
+ ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char),
+ ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT);
+#line 23 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 23 "sqljson_queryfuncs.pgc"
+
+ printf("Found json=%s\n", json);
+
+ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json_query ( jsonb '{\"a\": 123}' , '$' || '.' || 'a' with wrapper )", ECPGt_EOIT,
+ ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char),
+ ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT);
+#line 26 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 26 "sqljson_queryfuncs.pgc"
+
+ printf("Found json=%s\n", json);
+
+ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from json_table ( jsonb 'null' , '$[*]' as p0 columns ( nested '$' 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", ECPGt_EOIT, ECPGt_EORT);
+#line 39 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 39 "sqljson_queryfuncs.pgc"
+
+ // error
+
+ { ECPGdisconnect(__LINE__, "CURRENT");
+#line 42 "sqljson_queryfuncs.pgc"
+
+if (sqlca.sqlcode < 0) sqlprint();}
+#line 42 "sqljson_queryfuncs.pgc"
+
+
+ return 0;
+}
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stderr b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stderr
new file mode 100644
index 0000000000..05998a8195
--- /dev/null
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stderr
@@ -0,0 +1,46 @@
+[NO_PID]: ECPGdebug: set to 1
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ECPGconnect: opening database ecpg1_regression on <DEFAULT> port <DEFAULT>
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ECPGsetcommit on line 18: action "on"; connection "ecpg1_regression"
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 20: query: select json_exists ( jsonb '[]' , '$' ); with 0 parameter(s) on connection ecpg1_regression
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 20: using PQexec
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_process_output on line 20: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 20: RESULT: t offset: -1; array: no
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 23: query: select json_value ( jsonb '{"a": 123}' , '$' || '.' || 'b' default 'foo' on empty ); with 0 parameter(s) on connection ecpg1_regression
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 23: using PQexec
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_process_output on line 23: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 23: RESULT: foo offset: -1; array: no
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 26: query: select json_query ( jsonb '{"a": 123}' , '$' || '.' || 'a' with wrapper ); with 0 parameter(s) on connection ecpg1_regression
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 26: using PQexec
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_process_output on line 26: correctly got 1 tuples with 1 fields
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_is_type_an_array on line 26: type (3802); C (1); array (no)
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_get_data on line 26: RESULT: [123] offset: -1; array: no
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 29: query: select * from json_table ( jsonb 'null' , '$[*]' as p0 columns ( nested '$' 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; with 0 parameter(s) on connection ecpg1_regression
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_execute on line 29: using PQexec
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: ecpg_check_PQresult on line 29: bad response - ERROR: invalid JSON_TABLE plan
+LINE 1: ...ted path '$' as p21 columns ( baz int ) ) ) plan ( p1 ) ) jt
+ ^
+DETAIL: Path name mismatch: expected p0 but p1 is given.
+[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: raising sqlstate 42601 (sqlcode -400): invalid JSON_TABLE plan on line 29
+[NO_PID]: sqlca: code: -400, state: 42601
+SQL error: invalid JSON_TABLE plan on line 29
+[NO_PID]: ecpg_finish: connection ecpg1_regression closed
+[NO_PID]: sqlca: code: 0, state: 00000
diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stdout b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stdout
new file mode 100644
index 0000000000..e5e0613748
--- /dev/null
+++ b/src/interfaces/ecpg/test/expected/sql-sqljson_queryfuncs.stdout
@@ -0,0 +1,3 @@
+Found json=t
+Found json=foo
+Found json=[123]
diff --git a/src/interfaces/ecpg/test/sql/Makefile b/src/interfaces/ecpg/test/sql/Makefile
index d8213b25ce..96a0646877 100644
--- a/src/interfaces/ecpg/test/sql/Makefile
+++ b/src/interfaces/ecpg/test/sql/Makefile
@@ -24,6 +24,7 @@ TESTS = array array.c \
quote quote.c \
show show.c \
sqljson sqljson.c \
+ sqljson_queryfuncs sqljson_queryfuncs.c \
insupd insupd.c \
twophase twophase.c \
insupd insupd.c \
diff --git a/src/interfaces/ecpg/test/sql/meson.build b/src/interfaces/ecpg/test/sql/meson.build
index f4c9418abb..7229fa75c7 100644
--- a/src/interfaces/ecpg/test/sql/meson.build
+++ b/src/interfaces/ecpg/test/sql/meson.build
@@ -26,6 +26,7 @@ pgc_files = [
'show',
'sqlda',
'sqljson',
+ 'sqljson_queryfuncs',
'twophase',
]
diff --git a/src/interfaces/ecpg/test/sql/sqljson_queryfuncs.pgc b/src/interfaces/ecpg/test/sql/sqljson_queryfuncs.pgc
new file mode 100644
index 0000000000..8a2b7be1d1
--- /dev/null
+++ b/src/interfaces/ecpg/test/sql/sqljson_queryfuncs.pgc
@@ -0,0 +1,45 @@
+#include <stdio.h>
+
+EXEC SQL INCLUDE sqlca;
+exec sql include ../regression;
+
+EXEC SQL WHENEVER SQLERROR sqlprint;
+
+int
+main ()
+{
+EXEC SQL BEGIN DECLARE SECTION;
+ char json[1024];
+EXEC SQL END DECLARE SECTION;
+
+ ECPGdebug (1, stderr);
+
+ EXEC SQL CONNECT TO REGRESSDB1;
+ EXEC SQL SET AUTOCOMMIT = ON;
+
+ EXEC SQL SELECT JSON_EXISTS(jsonb '[]', '$') INTO :json;
+ printf("Found json=%s\n", json);
+
+ EXEC SQL SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY) INTO :json;
+ printf("Found json=%s\n", json);
+
+ EXEC SQL SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER) INTO :json;
+ printf("Found json=%s\n", json);
+
+ EXEC SQL SELECT * FROM JSON_TABLE(jsonb 'null', '$[*]' AS p0
+ COLUMNS (
+ NESTED '$' 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
+
+ EXEC SQL DISCONNECT;
+
+ return 0;
+}
--
2.35.3
[application/octet-stream] v28-0004-JSON_TABLE.patch (166.9K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/5-v28-0004-JSON_TABLE.patch)
download | inline diff:
From fdd231231b1305dd4319407fe3ff56678356a3f2 Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Wed, 22 Nov 2023 13:19:05 +0900
Subject: [PATCH v28 4/6] JSON_TABLE
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This feature allows jsonb data to be treated as a table and thus
used in a FROM clause like other tabular data. Data can be selected
from the jsonb using jsonpath expressions, and hoisted out of nested
structures in the jsonb to form multiple rows, more or less like an
outer join.
This also 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. The 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.
Author: Nikita Glukhov <[email protected]>
Author: Teodor Sigaev <[email protected]>
Author: Oleg Bartunov <[email protected]>
Author: Alexander Korotkov <[email protected]>
Author: Andrew Dunstan <[email protected]>
Author: Amit Langote <[email protected]>
Reviewers have included (in no particular order) Andres Freund, Alexander
Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu,
Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera,
jian he
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/[email protected]
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
---
doc/src/sgml/func.sgml | 496 ++++++++
src/backend/catalog/sql_features.txt | 6 +-
src/backend/commands/explain.c | 8 +-
src/backend/executor/execExprInterp.c | 6 +
src/backend/executor/nodeTableFuncscan.c | 27 +-
src/backend/nodes/makefuncs.c | 19 +
src/backend/nodes/nodeFuncs.c | 32 +
src/backend/parser/Makefile | 1 +
src/backend/parser/gram.y | 297 ++++-
src/backend/parser/meson.build | 1 +
src/backend/parser/parse_clause.c | 14 +-
src/backend/parser/parse_expr.c | 13 +
src/backend/parser/parse_jsontable.c | 769 ++++++++++++
src/backend/parser/parse_relation.c | 5 +-
src/backend/parser/parse_target.c | 3 +
src/backend/utils/adt/jsonpath_exec.c | 543 +++++++++
src/backend/utils/adt/ruleutils.c | 279 ++++-
src/include/nodes/execnodes.h | 2 +
src/include/nodes/makefuncs.h | 2 +
src/include/nodes/parsenodes.h | 95 ++
src/include/nodes/primnodes.h | 59 +-
src/include/parser/kwlist.h | 4 +
src/include/parser/parse_clause.h | 3 +
src/include/utils/jsonpath.h | 3 +
src/test/regress/expected/json_sqljson.out | 6 +
src/test/regress/expected/jsonb_sqljson.out | 1182 +++++++++++++++++++
src/test/regress/sql/json_sqljson.sql | 4 +
src/test/regress/sql/jsonb_sqljson.sql | 674 +++++++++++
src/tools/pgindent/typedefs.list | 13 +
29 files changed, 4537 insertions(+), 29 deletions(-)
create mode 100644 src/backend/parser/parse_jsontable.c
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7962a8a1a4..f8fa6817cd 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -17200,6 +17200,502 @@ array w/o UK? | t
</table>
</sect2>
+ <sect2 id="functions-sqljson-table">
+ <title>JSON_TABLE</title>
+ <indexterm>
+ <primary>json_table</primary>
+ </indexterm>
+
+ <para>
+ <function>json_table</function> is an SQL/JSON function which
+ queries <acronym>JSON</acronym> data
+ and presents the results as a relational view, which can be accessed as a
+ regular SQL table. You can only use <function>json_table</function> inside the
+ <literal>FROM</literal> clause of a <literal>SELECT</literal> statement.
+ </para>
+
+ <para>
+ Taking JSON data as input, <function>json_table</function> uses
+ a path expression to extract a part of the provided data that
+ will be used as a <firstterm>row pattern</firstterm> for the
+ constructed view. Each SQL/JSON item at the top level of the row pattern serves
+ as the source for a separate row in the constructed relational view.
+ </para>
+
+ <para>
+ To split the row pattern into columns, <function>json_table</function>
+ provides the <literal>COLUMNS</literal> clause that defines the
+ schema of the created view. For each column to be constructed,
+ this clause provides a separate path expression that evaluates
+ the row pattern, extracts a JSON item, and returns it as a
+ separate SQL value for the specified column. If the required value
+ is stored in a nested level of the row pattern, it can be extracted
+ using the <literal>NESTED PATH</literal> subclause. Joining the
+ columns returned by <literal>NESTED PATH</literal> can add multiple
+ new rows to the constructed view. Such rows are called
+ <firstterm>child rows</firstterm>, as opposed to the <firstterm>parent row</firstterm>
+ that generates them.
+ </para>
+
+ <para>
+ The rows produced by <function>JSON_TABLE</function> are laterally
+ joined to the row that generated them, so you do not have to explicitly join
+ the constructed view with the original table holding <acronym>JSON</acronym>
+ data. Optionally, you can specify how to join the columns returned
+ by <literal>NESTED PATH</literal> using the <literal>PLAN</literal> clause.
+ </para>
+
+ <para>
+ Each <literal>NESTED PATH</literal> clause can generate one or more
+ columns. Columns produced by <literal>NESTED PATH</literal>s at the
+ same level are considered to be <firstterm>siblings</firstterm>,
+ while a column produced by a <literal>NESTED PATH</literal> is
+ considered to be a child of the column produced by a
+ <literal>NESTED PATH</literal> or row expression at a higher level.
+ Sibling columns are always joined first. Once they are processed,
+ the resulting rows are joined to the parent row.
+ </para>
+
+ <para>
+ The syntax is:
+ </para>
+
+<synopsis>
+JSON_TABLE (
+ <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional>
+ COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> )
+ <optional> { <literal>ERROR</literal> | <literal>EMPTY</literal> } <literal>ON ERROR</literal> </optional>
+ <optional>
+ PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) |
+ PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional>
+ | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> )
+ </optional>
+)
+
+<phrase>
+where <replaceable class="parameter">json_table_column</replaceable> is:
+</phrase>
+ <replaceable>name</replaceable> <replaceable>type</replaceable> <optional> PATH <replaceable>json_path_specification</replaceable> </optional>
+ <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional>
+ <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional>
+ <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional>
+ <optional> { ERROR | NULL | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional>
+ | <replaceable>name</replaceable> <replaceable>type</replaceable> FORMAT <replaceable>json_representation</replaceable>
+ <optional> PATH <replaceable>json_path_specification</replaceable> </optional>
+ <optional> { WITHOUT | WITH { CONDITIONAL | <optional>UNCONDITIONAL</optional> } } <optional> ARRAY </optional> WRAPPER </optional>
+ <optional> { KEEP | OMIT } QUOTES <optional> ON SCALAR STRING </optional> </optional>
+ <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON EMPTY </optional>
+ <optional> { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT <replaceable>expression</replaceable> } ON ERROR </optional>
+ | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>json_path_specification</replaceable> </optional>
+ <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional>
+ | NESTED PATH <replaceable>json_path_specification</replaceable> <optional> AS <replaceable>path_name</replaceable> </optional>
+ COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> )
+ | <replaceable>name</replaceable> FOR ORDINALITY
+<phrase>
+<replaceable>json_table_plan</replaceable> is:
+</phrase>
+ <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional>
+ | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional>
+ | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional>
+<phrase>
+<replaceable>json_table_plan_primary</replaceable> is:
+</phrase>
+ <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> )
+</synopsis>
+
+ <para>
+ Each syntax element is described below in more detail.
+ </para>
+
+ <variablelist>
+ <varlistentry>
+ <term>
+ <literal><replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional></literal>
+ </term>
+ <listitem>
+ <para>
+ The input data to query, the JSON path expression defining the query,
+ and an optional <literal>PASSING</literal> clause, which can provide data
+ values to the <replaceable>path_expression</replaceable>.
+ The result of the input data
+ evaluation is called the <firstterm>row pattern</firstterm>. The row
+ pattern is used as the source for row values in the constructed view.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>COLUMNS</literal>( <replaceable>json_table_column</replaceable> <optional>, ...</optional> )
+ </term>
+ <listitem>
+
+ <para>
+ The <literal>COLUMNS</literal> clause defining the schema of the
+ constructed view. In this clause, you must specify all the columns
+ to be filled with SQL/JSON items.
+ The <replaceable>json_table_column</replaceable>
+ expression has the following syntax variants:
+ </para>
+
+ <variablelist>
+ <varlistentry>
+ <term>
+ <literal><replaceable>name</replaceable> <replaceable>type</replaceable>
+ <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional></literal>
+ </term>
+ <listitem>
+
+ <para>
+ Inserts a single SQL/JSON item into each row of
+ the specified column.
+ </para>
+ <para>
+ The provided <literal>PATH</literal> expression is evaluated and
+ the column is filled with the produced SQL/JSON items, one for each row.
+ If the <literal>PATH</literal> expression is omitted,
+ <function>JSON_TABLE</function> uses the
+ <literal>$.<replaceable>name</replaceable></literal> path expression,
+ where <replaceable>name</replaceable> is the provided column name.
+ In this case, the column name must correspond to one of the
+ keys within the SQL/JSON item produced by the row pattern.
+ </para>
+ <para>
+ Optionally, you can add <literal>ON EMPTY</literal> and
+ <literal>ON ERROR</literal> clauses to define how to handle missing values
+ or structural errors.
+ <literal>WRAPPER</literal> and <literal>QUOTES</literal> clauses can only
+ be used with JSON, array, and composite types.
+ These clauses have the same syntax and semantics as for
+ <function>json_value</function> and <function>json_query</function>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <replaceable>name</replaceable> <replaceable>type</replaceable> <literal>FORMAT</literal> <replaceable>json_representation</replaceable>
+ <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional>
+ </term>
+ <listitem>
+
+ <para>
+ Generates a column and inserts a composite SQL/JSON
+ item into each row of this column.
+ </para>
+ <para>
+ The provided <literal>PATH</literal> expression is evaluated and
+ the column is filled with the produced SQL/JSON items, one for each row.
+ If the <literal>PATH</literal> expression is omitted,
+ <function>JSON_TABLE</function> uses the
+ <literal>$.<replaceable>name</replaceable></literal> path expression,
+ where <replaceable>name</replaceable> is the provided column name.
+ In this case, the column name must correspond to one of the
+ keys within the SQL/JSON item produced by the row pattern.
+ </para>
+ <para>
+ Optionally, you can add <literal>WRAPPER</literal>, <literal>QUOTES</literal>,
+ <literal>ON EMPTY</literal> and <literal>ON ERROR</literal> clauses
+ to define additional settings for the returned SQL/JSON items.
+ These clauses have the same syntax and semantics as
+ for <function>json_query</function>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <replaceable>name</replaceable> <replaceable>type</replaceable>
+ <literal>EXISTS</literal> <optional> <literal>PATH</literal> <replaceable>json_path_specification</replaceable> </optional>
+ </term>
+ <listitem>
+
+ <para>
+ Generates a column and inserts a boolean item into each row of this column.
+ </para>
+ <para>
+ The provided <literal>PATH</literal> expression is evaluated,
+ a check whether any SQL/JSON items were returned is done, and
+ the column is filled with the resulting boolean value, one for each row.
+ The specified <parameter>type</parameter> should have a cast from the
+ <type>boolean</type>.
+ If the <literal>PATH</literal> expression is omitted,
+ <function>JSON_TABLE</function> uses the
+ <literal>$.<replaceable>name</replaceable></literal> path expression,
+ where <replaceable>name</replaceable> is the provided column name.
+ </para>
+ <para>
+ Optionally, you can add <literal>ON ERROR</literal> clause to define
+ error behavior. This clause has the same syntax and semantics as
+ for <function>json_exists</function>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>NESTED PATH</literal> <replaceable>json_path_specification</replaceable> <optional> <literal>AS</literal> <replaceable>json_path_name</replaceable> </optional>
+ <literal>COLUMNS</literal> ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> )
+ </term>
+ <listitem>
+
+ <para>
+ Extracts SQL/JSON items from nested levels of the row pattern,
+ generates one or more columns as defined by the <literal>COLUMNS</literal>
+ subclause, and inserts the extracted SQL/JSON items into each row of these columns.
+ The <replaceable>json_table_column</replaceable> expression in the
+ <literal>COLUMNS</literal> subclause uses the same syntax as in the
+ parent <literal>COLUMNS</literal> clause.
+ </para>
+
+ <para>
+ The <literal>NESTED PATH</literal> syntax is recursive,
+ so you can go down multiple nested levels by specifying several
+ <literal>NESTED PATH</literal> subclauses within each other.
+ It allows to unnest the hierarchy of JSON objects and arrays
+ in a single function invocation rather than chaining several
+ <function>JSON_TABLE</function> expressions in an SQL statement.
+ </para>
+
+ <para>
+ You can use the <literal>PLAN</literal> clause to define how
+ to join the columns returned by <literal>NESTED PATH</literal> clauses.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <replaceable>name</replaceable> <literal>FOR ORDINALITY</literal>
+ </term>
+ <listitem>
+
+ <para>
+ Adds an ordinality column that provides sequential row numbering.
+ You can have only one ordinality column per table. Row numbering
+ is 1-based. For child rows that result from the <literal>NESTED PATH</literal>
+ clauses, the parent row number is repeated.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>AS</literal> <replaceable>json_path_name</replaceable>
+ </term>
+ <listitem>
+
+ <para>
+ The optional <replaceable>json_path_name</replaceable> serves as an
+ identifier of the provided <replaceable>json_path_specification</replaceable>.
+ The path name must be unique and distinct from the column names.
+ When using the <literal>PLAN</literal> clause, you must specify the names
+ for all the paths, including the row pattern. Each path name can appear in
+ the <literal>PLAN</literal> clause only once.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> )
+ </term>
+ <listitem>
+
+ <para>
+ Defines how to join the data returned by <literal>NESTED PATH</literal>
+ clauses to the constructed view.
+ </para>
+ <para>
+ To join columns with parent/child relationship, you can use:
+ </para>
+ <variablelist>
+ <varlistentry>
+ <term>
+ <literal>INNER</literal>
+ </term>
+ <listitem>
+
+ <para>
+ Use <literal>INNER JOIN</literal>, so that the parent row
+ is omitted from the output if it does not have any child rows
+ after joining the data returned by <literal>NESTED PATH</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>OUTER</literal>
+ </term>
+ <listitem>
+
+ <para>
+ Use <literal>LEFT OUTER JOIN</literal>, 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 <literal>NESTED PATH</literal>, with NULL values
+ inserted into the child columns if the corresponding
+ values are missing.
+ </para>
+ <para>
+ This is the default option for joining columns with parent/child relationship.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>
+ To join sibling columns, you can use:
+ </para>
+
+ <variablelist>
+ <varlistentry>
+ <term>
+ <literal>UNION</literal>
+ </term>
+ <listitem>
+
+ <para>
+ Generate one row for each value produced by each of the sibling
+ columns. The columns from the other siblings are set to null.
+ </para>
+ <para>
+ This is the default option for joining sibling columns.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>CROSS</literal>
+ </term>
+ <listitem>
+
+ <para>
+ Generate one row for each combination of values from the sibling columns.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> )
+ </term>
+ <listitem>
+ <para>
+ The terms can also be specified in reverse order. The
+ <literal>INNER</literal> or <literal>OUTER</literal> option defines the
+ joining plan for parent/child columns, while <literal>UNION</literal> or
+ <literal>CROSS</literal> affects joins of sibling columns. This form
+ of <literal>PLAN</literal> overrides the default plan for
+ all columns at once. Even though the path names are not included in the
+ <literal>PLAN DEFAULT</literal> form, to conform to the SQL/JSON standard
+ they must be provided for all the paths if the <literal>PLAN</literal>
+ clause is used.
+ </para>
+ <para>
+ <literal>PLAN DEFAULT</literal> is simpler than specifying a complete
+ <literal>PLAN</literal>, and is often all that is required to get the desired
+ output.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>Examples</para>
+
+ <para>
+ In these examples the following small table storing some JSON data will be used:
+<programlisting>
+CREATE TABLE my_films ( js jsonb );
+
+INSERT INTO my_films VALUES (
+'{ "favorites" : [
+ { "kind" : "comedy", "films" : [
+ { "title" : "Bananas",
+ "director" : "Woody Allen"},
+ { "title" : "The Dinner Game",
+ "director" : "Francis Veber" } ] },
+ { "kind" : "horror", "films" : [
+ { "title" : "Psycho",
+ "director" : "Alfred Hitchcock" } ] },
+ { "kind" : "thriller", "films" : [
+ { "title" : "Vertigo",
+ "director" : "Alfred Hitchcock" } ] },
+ { "kind" : "drama", "films" : [
+ { "title" : "Yojimbo",
+ "director" : "Akira Kurosawa" } ] }
+ ] }');
+</programlisting>
+ </para>
+ <para>
+ Query the <structname>my_films</structname> table holding
+ some JSON data about the films and create a view that
+ distributes the film genre, title, and director between separate columns:
+<screen>
+SELECT jt.* FROM
+ my_films,
+ JSON_TABLE ( js, '$.favorites[*]' COLUMNS (
+ id FOR ORDINALITY,
+ kind text PATH '$.kind',
+ NESTED PATH '$.films[*]' COLUMNS (
+ title text PATH '$.title',
+ director text PATH '$.director'))) AS jt;
+----+----------+------------------+-------------------
+ id | kind | title | director
+----+----------+------------------+-------------------
+ 1 | comedy | Bananas | Woody Allen
+ 1 | comedy | The Dinner Game | Francis Veber
+ 2 | horror | Psycho | Alfred Hitchcock
+ 3 | thriller | Vertigo | Alfred Hitchcock
+ 4 | drama | Yojimbo | Akira Kurosawa
+ (5 rows)
+</screen>
+ </para>
+
+ <para>
+ Find a director that has done films in two different genres:
+<screen>
+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)
+</screen>
+ </para>
+ </sect2>
+
<sect2 id="functions-sqljson-path">
<title>The SQL/JSON Path Language</title>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 7598bd8f22..9500a80f4d 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -551,10 +551,10 @@ 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 NO
+T827 JSON_TABLE: sibling NESTED COLUMNS clauses YES
T828 JSON_QUERY YES
T829 JSON_QUERY: array wrapper options YES
T830 Enforcing unique keys in SQL/JSON constructor functions YES
@@ -565,7 +565,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/commands/explain.c b/src/backend/commands/explain.c
index f1d71bc54e..8e35525781 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3892,7 +3892,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
break;
case T_TableFuncScan:
Assert(rte->rtekind == RTE_TABLEFUNC);
- objectname = "xmltable";
+ if (rte->tablefunc)
+ if (rte->tablefunc->functype == TFT_XMLTABLE)
+ objectname = "xmltable";
+ else /* Must be TFT_JSON_TABLE */
+ objectname = "json_table";
+ else
+ objectname = NULL;
objecttag = "Table Function Name";
break;
case T_ValuesScan:
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 697b9a84a9..8f9077d7dd 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4345,6 +4345,12 @@ ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op,
break;
}
+ case JSON_TABLE_OP:
+ post_eval->jump_eval_coercion = -1;
+ *op->resvalue = item;
+ *op->resnull = false;
+ break;
+
default:
elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op);
return false;
diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c
index a60dcd4943..0d7f518afd 100644
--- a/src/backend/executor/nodeTableFuncscan.c
+++ b/src/backend/executor/nodeTableFuncscan.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "nodes/execnodes.h"
#include "utils/builtins.h"
+#include "utils/jsonpath.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/xml.h"
@@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags)
scanstate->ss.ps.qual =
ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps);
- /* Only XMLTABLE is supported currently */
- scanstate->routine = &XmlTableRoutine;
+ /* Only XMLTABLE and JSON_TABLE are supported currently */
+ scanstate->routine =
+ tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine;
scanstate->perTableCxt =
AllocSetContextCreate(CurrentMemoryContext,
@@ -182,6 +184,10 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags)
ExecInitExprList(tf->colexprs, (PlanState *) scanstate);
scanstate->coldefexprs =
ExecInitExprList(tf->coldefexprs, (PlanState *) scanstate);
+ scanstate->colvalexprs =
+ ExecInitExprList(tf->colvalexprs, (PlanState *) scanstate);
+ scanstate->passingvalexprs =
+ ExecInitExprList(tf->passingvalexprs, (PlanState *) scanstate);
scanstate->notnulls = tf->notnulls;
@@ -369,14 +375,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc)
routine->SetNamespace(tstate, ns_name, ns_uri);
}
- /* Install the row filter expression into the table builder context */
- value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull);
- if (isnull)
- ereport(ERROR,
- (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
- errmsg("row filter expression must not be null")));
+ if (routine->SetRowFilter)
+ {
+ /* Install the row filter expression into the table builder context */
+ value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull);
+ if (isnull)
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("row filter expression must not be null")));
- routine->SetRowFilter(tstate, TextDatumGetCString(value));
+ routine->SetRowFilter(tstate, TextDatumGetCString(value));
+ }
/*
* Install the column filter expressions into the table builder context.
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index e466fe5176..e870a5df73 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -875,6 +875,25 @@ makeJsonBehavior(JsonBehaviorType type, Node *expr, JsonCoercion *coercion,
return behavior;
}
+/*
+ * makeJsonTableJoinedPlan -
+ * creates a joined JsonTablePlan node
+ */
+Node *
+makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2,
+ int location)
+{
+ JsonTablePlan *n = makeNode(JsonTablePlan);
+
+ n->plan_type = JSTP_JOINED;
+ n->join_type = type;
+ n->plan1 = castNode(JsonTablePlan, plan1);
+ n->plan2 = castNode(JsonTablePlan, plan2);
+ n->location = location;
+
+ return (Node *) n;
+}
+
/*
* makeJsonEncoding -
* converts JSON encoding name to enum JsonEncoding
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 62bf08ad27..f16bdd95a0 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2697,6 +2697,10 @@ expression_tree_walker_impl(Node *node,
return true;
if (WALK(tf->coldefexprs))
return true;
+ if (WALK(tf->colvalexprs))
+ return true;
+ if (WALK(tf->passingvalexprs))
+ return true;
}
break;
default:
@@ -3757,6 +3761,8 @@ expression_tree_mutator_impl(Node *node,
MUTATE(newnode->rowexpr, tf->rowexpr, Node *);
MUTATE(newnode->colexprs, tf->colexprs, List *);
MUTATE(newnode->coldefexprs, tf->coldefexprs, List *);
+ MUTATE(newnode->colvalexprs, tf->colvalexprs, List *);
+ MUTATE(newnode->passingvalexprs, tf->passingvalexprs, List *);
return (Node *) newnode;
}
break;
@@ -4179,6 +4185,32 @@ raw_expression_tree_walker_impl(Node *node,
return true;
}
break;
+ case T_JsonTable:
+ {
+ JsonTable *jt = (JsonTable *) node;
+
+ if (WALK(jt->context_item))
+ return true;
+ if (WALK(jt->pathspec))
+ return true;
+ if (WALK(jt->passing))
+ return true;
+ if (WALK(jt->columns))
+ return true;
+ }
+ break;
+ case T_JsonTableColumn:
+ {
+ JsonTableColumn *jtc = (JsonTableColumn *) node;
+
+ if (WALK(jtc->typeName))
+ return true;
+ if (WALK(jtc->behavior))
+ return true;
+ if (jtc->coltype == JTC_NESTED && WALK(jtc->columns))
+ return true;
+ }
+ break;
case T_NullTest:
return WALK(((NullTest *) node)->arg);
case T_BooleanTest:
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 401c16686c..3162a01f30 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -23,6 +23,7 @@ OBJS = \
parse_enr.o \
parse_expr.o \
parse_func.o \
+ parse_jsontable.o \
parse_merge.o \
parse_node.o \
parse_oper.o \
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4f92d000ec..3755434af0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -652,15 +652,32 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
json_aggregate_func
json_argument
json_behavior
+ json_table
+ json_table_column_definition
+ json_table_plan_clause_opt
+ json_table_plan
+ json_table_plan_simple
+ json_table_plan_parent_child
+ json_table_plan_outer
+ json_table_plan_inner
+ json_table_plan_sibling
+ json_table_plan_union
+ json_table_plan_cross
+ json_table_plan_primary
%type <list> json_name_and_value_list
json_value_expr_list
json_array_aggregate_order_by_clause_opt
json_arguments
json_behavior_clause_opt
json_passing_clause_opt
+ json_table_column_definition_list
+%type <str> json_table_column_path_specification_clause_opt
%type <ival> json_encoding_clause_opt
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 <boolean> json_key_uniqueness_constraint_opt
json_object_constructor_null_clause_opt
@@ -730,7 +747,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG
- JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE
+ JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE
KEEP KEY KEYS
@@ -741,8 +758,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
MINUTE_P MINVALUE MODE MONTH_P MOVE
- NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
- NORMALIZE NORMALIZED
+ NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO
+ NONE NORMALIZE NORMALIZED
NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
NULLS_P NUMERIC
@@ -750,8 +767,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
ORDER ORDINALITY OTHERS OUT_P OUTER_P
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
- PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
- PLACING PLANS POLICY
+ PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH
+ PLACING PLAN PLANS POLICY
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
@@ -894,6 +911,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
*/
%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL
+%nonassoc NESTED
+%left PATH
%%
/*
@@ -13420,6 +13439,21 @@ table_ref: relation_expr opt_alias_clause
$2->alias = $4;
$$ = (Node *) $2;
}
+ | json_table opt_alias_clause
+ {
+ JsonTable *jt = castNode(JsonTable, $1);
+
+ jt->alias = $2;
+ $$ = (Node *) jt;
+ }
+ | LATERAL_P json_table opt_alias_clause
+ {
+ JsonTable *jt = castNode(JsonTable, $2);
+
+ jt->alias = $3;
+ jt->lateral = true;
+ $$ = (Node *) jt;
+ }
;
@@ -13987,6 +14021,8 @@ xmltable_column_option_el:
{ $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); }
| NULL_P
{ $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); }
+ | PATH b_expr
+ { $$ = makeDefElem("path", $2, @1); }
;
xml_namespace_list:
@@ -16622,6 +16658,249 @@ json_quotes_clause_opt:
| /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; }
;
+json_table:
+ JSON_TABLE '('
+ json_value_expr ',' a_expr json_passing_clause_opt
+ COLUMNS '(' json_table_column_definition_list ')'
+ json_table_plan_clause_opt
+ json_behavior_clause_opt
+ ')'
+ {
+ JsonTable *n = makeNode(JsonTable);
+
+ n->context_item = (JsonValueExpr *) $3;
+ n->pathspec = $5;
+ n->pathname = NULL;
+ n->passing = $6;
+ n->columns = $9;
+ n->plan = (JsonTablePlan *) $11;
+ n->behavior = $12;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | JSON_TABLE '('
+ json_value_expr ',' a_expr AS name json_passing_clause_opt
+ COLUMNS '(' json_table_column_definition_list ')'
+ json_table_plan_clause_opt
+ json_behavior_clause_opt
+ ')'
+ {
+ JsonTable *n = makeNode(JsonTable);
+
+ n->context_item = (JsonValueExpr *) $3;
+ n->pathspec = $5;
+ n->pathname = $7;
+ n->passing = $8;
+ n->columns = $11;
+ n->plan = (JsonTablePlan *) $13;
+ n->behavior = $14;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
+json_table_column_definition_list:
+ json_table_column_definition
+ { $$ = list_make1($1); }
+ | json_table_column_definition_list ',' json_table_column_definition
+ { $$ = lappend($1, $3); }
+ ;
+
+json_table_column_definition:
+ ColId FOR ORDINALITY
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_FOR_ORDINALITY;
+ n->name = $1;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | ColId Typename
+ json_table_column_path_specification_clause_opt
+ json_wrapper_behavior
+ json_quotes_clause_opt
+ json_behavior_clause_opt
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_REGULAR;
+ n->name = $1;
+ n->typeName = $2;
+ n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
+ n->pathspec = $3;
+ n->wrapper = $4; /* disallowed during parse-analysis! */
+ n->quotes = $5; /* disallowed during parse-analysis! */
+ n->behavior = $6;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | ColId Typename FORMAT_LA JSON json_encoding_clause_opt
+ json_table_column_path_specification_clause_opt
+ json_wrapper_behavior
+ json_quotes_clause_opt
+ json_behavior_clause_opt
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_FORMATTED;
+ n->name = $1;
+ n->typeName = $2;
+ n->format = makeJsonFormat(JS_FORMAT_JSON, $5, @1);
+ n->pathspec = $6;
+ n->wrapper = $7;
+ n->quotes = $8;
+ n->behavior = $9;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | ColId Typename
+ EXISTS json_table_column_path_specification_clause_opt
+ json_behavior_clause_opt
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_EXISTS;
+ n->name = $1;
+ n->typeName = $2;
+ n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
+ n->wrapper = JSW_NONE;
+ n->quotes = JS_QUOTES_UNSPEC;
+ n->pathspec = $4;
+ n->behavior = $5;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NESTED path_opt Sconst
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $3;
+ n->pathname = NULL;
+ n->columns = $6;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | NESTED path_opt Sconst AS name
+ COLUMNS '(' json_table_column_definition_list ')'
+ {
+ JsonTableColumn *n = makeNode(JsonTableColumn);
+
+ n->coltype = JTC_NESTED;
+ n->pathspec = $3;
+ n->pathname = $5;
+ n->columns = $8;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
+json_table_column_path_specification_clause_opt:
+ PATH Sconst { $$ = $2; }
+ | /* EMPTY */ { $$ = NULL; }
+ ;
+
+path_opt:
+ PATH { }
+ | /* EMPTY */ { }
+ ;
+
+json_table_plan_clause_opt:
+ PLAN '(' json_table_plan ')' { $$ = $3; }
+ | PLAN DEFAULT '(' json_table_default_plan_choices ')'
+ {
+ JsonTablePlan *n = makeNode(JsonTablePlan);
+
+ n->plan_type = JSTP_DEFAULT;
+ n->join_type = $4;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | /* EMPTY */ { $$ = NULL; }
+ ;
+
+json_table_plan:
+ json_table_plan_simple
+ | json_table_plan_parent_child
+ | json_table_plan_sibling
+ ;
+
+json_table_plan_simple:
+ name
+ {
+ JsonTablePlan *n = makeNode(JsonTablePlan);
+
+ n->plan_type = JSTP_SIMPLE;
+ n->pathname = $1;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ ;
+
+json_table_plan_primary:
+ json_table_plan_simple { $$ = $1; }
+ | '(' json_table_plan ')'
+ {
+ castNode(JsonTablePlan, $2)->location = @1;
+ $$ = $2;
+ }
+ ;
+
+json_table_plan_outer:
+ json_table_plan_simple OUTER_P json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_OUTER, $1, $3, @1); }
+ ;
+
+json_table_plan_inner:
+ json_table_plan_simple INNER_P json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); }
+ ;
+
+json_table_plan_parent_child:
+ json_table_plan_outer
+ | json_table_plan_inner
+ ;
+
+json_table_plan_union:
+ json_table_plan_primary UNION json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); }
+ | json_table_plan_union UNION json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); }
+ ;
+
+json_table_plan_cross:
+ json_table_plan_primary CROSS json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); }
+ | json_table_plan_cross CROSS json_table_plan_primary
+ { $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); }
+ ;
+
+json_table_plan_sibling:
+ json_table_plan_union
+ | json_table_plan_cross
+ ;
+
+json_table_default_plan_choices:
+ json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; }
+ | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_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 { $$ = JSTPJ_INNER; }
+ | OUTER_P { $$ = JSTPJ_OUTER; }
+ ;
+
+json_table_default_plan_union_cross:
+ UNION { $$ = JSTPJ_UNION; }
+ | CROSS { $$ = JSTPJ_CROSS; }
+ ;
+
json_returning_clause_opt:
RETURNING Typename json_format_clause_opt
{
@@ -17393,6 +17672,7 @@ unreserved_keyword:
| MOVE
| NAME_P
| NAMES
+ | NESTED
| NEW
| NEXT
| NFC
@@ -17427,6 +17707,8 @@ unreserved_keyword:
| PARTITION
| PASSING
| PASSWORD
+ | PATH
+ | PLAN
| PLANS
| POLICY
| PRECEDING
@@ -17591,6 +17873,7 @@ col_name_keyword:
| JSON_QUERY
| JSON_SCALAR
| JSON_SERIALIZE
+ | JSON_TABLE
| JSON_VALUE
| LEAST
| NATIONAL
@@ -17959,6 +18242,7 @@ bare_label_keyword:
| JSON_QUERY
| JSON_SCALAR
| JSON_SERIALIZE
+ | JSON_TABLE
| JSON_VALUE
| KEEP
| KEY
@@ -17998,6 +18282,7 @@ bare_label_keyword:
| NATIONAL
| NATURAL
| NCHAR
+ | NESTED
| NEW
| NEXT
| NFC
@@ -18042,7 +18327,9 @@ bare_label_keyword:
| PARTITION
| PASSING
| PASSWORD
+ | PATH
| PLACING
+ | PLAN
| PLANS
| POLICY
| POSITION
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 94517a7f7d..0f0e4ac7ca 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -10,6 +10,7 @@ backend_sources += files(
'parse_enr.c',
'parse_expr.c',
'parse_func.c',
+ 'parse_jsontable.c',
'parse_merge.c',
'parse_node.c',
'parse_oper.c',
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 334b9b42bd..affd812619 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -697,7 +697,11 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf)
char **names;
int colno;
- /* Currently only XMLTABLE is supported */
+ /*
+ * Currently we only support XMLTABLE here. See transformJsonTable() for
+ * JSON_TABLE support.
+ */
+ tf->functype = TFT_XMLTABLE;
constructName = "XMLTABLE";
docType = XMLOID;
@@ -1104,13 +1108,17 @@ transformFromClauseItem(ParseState *pstate, Node *n,
rtr->rtindex = nsitem->p_rtindex;
return (Node *) rtr;
}
- else if (IsA(n, RangeTableFunc))
+ else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable))
{
/* table function is like a plain relation */
RangeTblRef *rtr;
ParseNamespaceItem *nsitem;
- nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n);
+ if (IsA(n, RangeTableFunc))
+ nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n);
+ else
+ nsitem = transformJsonTable(pstate, (JsonTable *) n);
+
*top_nsitem = nsitem;
*namespace = list_make1(nsitem);
rtr = makeNode(RangeTblRef);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 55a7680001..c1c86f5796 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -4366,6 +4366,19 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
JSON_BEHAVIOR_NULL,
jsexpr->returning);
break;
+
+ case JSON_TABLE_OP:
+ func_name = "JSON_TABLE";
+
+ jsexpr = transformJsonExprCommon(pstate, func, func_name);
+ jsexpr->returning = makeNode(JsonReturning);
+ jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
+ jsexpr->returning->typid = exprType(jsexpr->formatted_expr);
+ jsexpr->returning->typmod = -1;
+ jsexpr->on_error = transformJsonBehavior(pstate, on_error,
+ JSON_BEHAVIOR_EMPTY,
+ jsexpr->returning);
+ break;
}
return (Node *) jsexpr;
diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c
new file mode 100644
index 0000000000..9144148120
--- /dev/null
+++ b/src/backend/parser/parse_jsontable.c
@@ -0,0 +1,769 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_jsontable.c
+ * parsing of JSON_TABLE
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/parser/parse_jsontable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "catalog/pg_collation.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/optimizer.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_relation.h"
+#include "parser/parse_type.h"
+#include "utils/builtins.h"
+#include "utils/json.h"
+#include "utils/lsyscache.h"
+
+/* Context for JSON_TABLE transformation */
+typedef struct JsonTableParseContext
+{
+ ParseState *pstate; /* parsing state */
+ JsonTable *table; /* untransformed node */
+ TableFunc *tablefunc; /* transformed node */
+ List *pathNames; /* list of all path and columns names */
+ int pathNameId; /* path name id counter */
+ Oid contextItemTypid; /* type oid of context item (json/jsonb) */
+} JsonTableParseContext;
+
+static JsonTableParent *transformJsonTableColumns(JsonTableParseContext * cxt,
+ JsonTablePlan *plan,
+ List *columns,
+ char *pathSpec,
+ char **pathName,
+ int location);
+
+static Node *
+makeStringConst(char *str, int location)
+{
+ A_Const *n = makeNode(A_Const);
+
+ n->val.node.type = T_String;
+ n->val.sval.sval = str;
+ n->location = location;
+
+ return (Node *) n;
+}
+
+/*
+ * Transform JSON_TABLE column
+ * - regular column into JSON_VALUE()
+ * - FORMAT JSON column into JSON_QUERY()
+ * - EXISTS column into JSON_EXISTS()
+ */
+static Node *
+transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
+ List *passingArgs, bool errorOnError)
+{
+ JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr);
+ JsonOutput *output = makeNode(JsonOutput);
+ char *pathspec;
+ JsonFormat *default_format;
+
+ jfexpr->op =
+ jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP :
+ jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP;
+ jfexpr->output = output;
+ jfexpr->behavior = jtc->behavior;
+ if ((jfexpr->behavior == NIL ||
+ lsecond(jfexpr->behavior) == NULL) &&
+ errorOnError)
+ {
+ JsonBehavior *on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL,
+ NULL, -1);
+
+ if (jfexpr->behavior == NIL)
+ jfexpr->behavior = list_make2(NULL, on_error);
+ else
+ {
+ jfexpr->behavior = list_delete_last(jfexpr->behavior);
+ jfexpr->behavior = lappend(jfexpr->behavior, on_error);
+ }
+ }
+ jfexpr->quotes = jtc->quotes;
+ jfexpr->wrapper = jtc->wrapper;
+ jfexpr->location = jtc->location;
+
+ output->typeName = jtc->typeName;
+ output->returning = makeNode(JsonReturning);
+ output->returning->format = jtc->format;
+
+ default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
+
+ if (jtc->pathspec)
+ pathspec = jtc->pathspec;
+ else
+ {
+ /* Construct default path as '$."column_name"' */
+ StringInfoData path;
+
+ initStringInfo(&path);
+
+ appendStringInfoString(&path, "$.");
+ escape_json(&path, jtc->name);
+
+ pathspec = path.data;
+ }
+
+ jfexpr->context_item = makeJsonValueExpr((Expr *) contextItemExpr, NULL,
+ default_format);
+ jfexpr->pathspec = makeStringConst(pathspec, -1);
+ jfexpr->pathname = NULL;
+ jfexpr->passing = passingArgs;
+
+ return (Node *) jfexpr;
+}
+
+static bool
+isJsonTablePathNameDuplicate(JsonTableParseContext * cxt, const char *pathname)
+{
+ ListCell *lc;
+
+ foreach(lc, cxt->pathNames)
+ {
+ if (!strcmp(pathname, (const char *) lfirst(lc)))
+ return true;
+ }
+
+ return false;
+}
+
+/* Register the column name in the path name list. */
+static void
+registerJsonTableColumn(JsonTableParseContext * cxt, char *colname)
+{
+ if (isJsonTablePathNameDuplicate(cxt, colname))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_ALIAS),
+ errmsg("duplicate JSON_TABLE column name: %s", colname),
+ errhint("JSON_TABLE column names must be distinct from one another.")));
+
+ cxt->pathNames = lappend(cxt->pathNames, colname);
+}
+
+/* Recursively register all nested column names in the path name list. */
+static void
+registerAllJsonTableColumns(JsonTableParseContext * cxt, List *columns)
+{
+ ListCell *lc;
+
+ foreach(lc, columns)
+ {
+ JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc));
+
+ if (jtc->coltype == JTC_NESTED)
+ {
+ if (jtc->pathname)
+ registerJsonTableColumn(cxt, jtc->pathname);
+
+ registerAllJsonTableColumns(cxt, jtc->columns);
+ }
+ else
+ {
+ registerJsonTableColumn(cxt, jtc->name);
+ }
+ }
+}
+
+/* Generate a new unique JSON_TABLE path name. */
+static char *
+generateJsonTablePathName(JsonTableParseContext * cxt)
+{
+ char namebuf[32];
+ char *name = namebuf;
+
+ do
+ {
+ snprintf(namebuf, sizeof(namebuf), "json_table_path_%d",
+ ++cxt->pathNameId);
+ } while (isJsonTablePathNameDuplicate(cxt, name));
+
+ name = pstrdup(name);
+ cxt->pathNames = lappend(cxt->pathNames, name);
+
+ return name;
+}
+
+/* Collect sibling path names from plan to the specified list. */
+static void
+collectSiblingPathsInJsonTablePlan(JsonTablePlan *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 == JSTPJ_INNER ||
+ plan->join_type == JSTPJ_OUTER)
+ {
+ Assert(plan->plan1->plan_type == JSTP_SIMPLE);
+ *paths = lappend(*paths, plan->plan1->pathname);
+ }
+ else if (plan->join_type == JSTPJ_CROSS ||
+ plan->join_type == JSTPJ_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(ParseState *pstate, JsonTablePlan *plan,
+ List *columns)
+{
+ 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;
+
+ if (!jtc->pathname)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("nested JSON_TABLE columns must contain an explicit AS pathname specification if an explicit PLAN clause is used"),
+ parser_errposition(pstate, jtc->location)));
+
+ /* find nested path name in the list of sibling path names */
+ foreach(lc2, siblings)
+ {
+ if ((found = !strcmp(jtc->pathname, lfirst(lc2))))
+ break;
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE plan"),
+ errdetail("Plan node for nested path %s was not found in plan.", jtc->pathname),
+ parser_errposition(pstate, jtc->location)));
+
+ nchildren++;
+ }
+ }
+
+ if (list_length(siblings) > nchildren)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE plan"),
+ errdetail("Plan node contains some extra or duplicate sibling nodes."),
+ parser_errposition(pstate, plan ? plan->location : -1)));
+}
+
+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->pathname &&
+ !strcmp(jtc->pathname, pathname))
+ return jtc;
+ }
+
+ return NULL;
+}
+
+static Node *
+transformNestedJsonTableColumn(JsonTableParseContext * cxt, JsonTableColumn *jtc,
+ JsonTablePlan *plan)
+{
+ JsonTableParent *node;
+ char *pathname = jtc->pathname;
+
+ node = transformJsonTableColumns(cxt, plan, jtc->columns, jtc->pathspec,
+ &pathname, jtc->location);
+
+ return (Node *) node;
+}
+
+static Node *
+makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode)
+{
+ JsonTableSibling *join = makeNode(JsonTableSibling);
+
+ join->larg = lnode;
+ join->rarg = rnode;
+ join->cross = cross;
+
+ return (Node *) join;
+}
+
+/*
+ * Recursively transform child JSON_TABLE plan.
+ *
+ * Default plan is transformed into a cross/union join of its nested columns.
+ * Simple and outer/inner plans are transformed into a JsonTableParent by
+ * finding and transforming corresponding nested column.
+ * Sibling plans are recursively transformed into a JsonTableSibling.
+ */
+static Node *
+transformJsonTableChildPlan(JsonTableParseContext * cxt, JsonTablePlan *plan,
+ List *columns)
+{
+ JsonTableColumn *jtc = NULL;
+
+ if (!plan || plan->plan_type == JSTP_DEFAULT)
+ {
+ /* unspecified or default plan */
+ Node *res = NULL;
+ ListCell *lc;
+ bool cross = plan && (plan->join_type & JSTPJ_CROSS);
+
+ /* transform all nested columns into cross/union join */
+ foreach(lc, columns)
+ {
+ JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc));
+ Node *node;
+
+ if (col->coltype != JTC_NESTED)
+ continue;
+
+ node = transformNestedJsonTableColumn(cxt, col, plan);
+
+ /* join transformed node with previous sibling nodes */
+ res = res ? makeJsonTableSiblingJoin(cross, res, node) : node;
+ }
+
+ return res;
+ }
+ else if (plan->plan_type == JSTP_SIMPLE)
+ {
+ jtc = findNestedJsonTableColumn(columns, plan->pathname);
+ }
+ else if (plan->plan_type == JSTP_JOINED)
+ {
+ if (plan->join_type == JSTPJ_INNER ||
+ plan->join_type == JSTPJ_OUTER)
+ {
+ Assert(plan->plan1->plan_type == JSTP_SIMPLE);
+ jtc = findNestedJsonTableColumn(columns, plan->plan1->pathname);
+ }
+ else
+ {
+ Node *node1 = transformJsonTableChildPlan(cxt, plan->plan1,
+ columns);
+ Node *node2 = transformJsonTableChildPlan(cxt, plan->plan2,
+ columns);
+
+ return makeJsonTableSiblingJoin(plan->join_type == JSTPJ_CROSS,
+ node1, node2);
+ }
+ }
+ else
+ elog(ERROR, "invalid JSON_TABLE plan type %d", plan->plan_type);
+
+ if (!jtc)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE plan"),
+ errdetail("Path name was %s not found in nested columns list.",
+ plan->pathname),
+ parser_errposition(cxt->pstate, plan->location)));
+
+ return transformNestedJsonTableColumn(cxt, jtc, plan);
+}
+
+/* Check whether type is json/jsonb, array, or record. */
+static bool
+typeIsComposite(Oid typid)
+{
+ char typtype;
+
+ if (typid == JSONOID ||
+ typid == JSONBOID ||
+ typid == RECORDOID ||
+ type_is_array(typid))
+ return true;
+
+ typtype = get_typtype(typid);
+
+ if (typtype == TYPTYPE_COMPOSITE)
+ return true;
+
+ if (typtype == TYPTYPE_DOMAIN)
+ return typeIsComposite(getBaseType(typid));
+
+ return false;
+}
+
+/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */
+static void
+appendJsonTableColumns(JsonTableParseContext * cxt, List *columns)
+{
+ ListCell *col;
+ ParseState *pstate = cxt->pstate;
+ JsonTable *jt = cxt->table;
+ TableFunc *tf = cxt->tablefunc;
+ bool ordinality_found = false;
+ JsonBehavior *on_error = jt->behavior != NIL ? lsecond(jt->behavior) : NULL;
+ bool errorOnError = on_error &&
+ on_error->btype == JSON_BEHAVIOR_ERROR;
+
+ foreach(col, columns)
+ {
+ JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col));
+ Oid typid;
+ int32 typmod;
+ Node *colexpr;
+
+ if (rawc->name)
+ {
+ /* make sure column names are unique */
+ ListCell *colname;
+
+ foreach(colname, tf->colnames)
+ if (!strcmp((const char *) colname, rawc->name))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("column name \"%s\" is not unique",
+ rawc->name),
+ parser_errposition(pstate, rawc->location)));
+
+ tf->colnames = lappend(tf->colnames,
+ makeString(pstrdup(rawc->name)));
+ }
+
+ /*
+ * Determine the type and typmod for the new column. FOR ORDINALITY
+ * columns are INTEGER by standard; the others are user-specified.
+ */
+ switch (rawc->coltype)
+ {
+ case JTC_FOR_ORDINALITY:
+ if (ordinality_found)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use more than one FOR ORDINALITY column"),
+ parser_errposition(pstate, rawc->location)));
+ ordinality_found = true;
+ colexpr = NULL;
+ typid = INT4OID;
+ typmod = -1;
+ break;
+
+ case JTC_REGULAR:
+ typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod);
+
+ /*
+ * Use implicit FORMAT JSON for composite types (arrays and
+ * records).
+ *
+ * For scalar columns, require an explicit FORMAT JSON clause
+ * to get the specified WRAPPER and QUOTES behavior.
+ */
+ if (typeIsComposite(typid))
+ rawc->coltype = JTC_FORMATTED;
+ else if (rawc->wrapper != JSW_NONE)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use WITH WRAPPER clause for scalar columns"
+ " without also specifying FORMAT clause"),
+ parser_errposition(pstate, rawc->location)));
+ else if (rawc->quotes != JS_QUOTES_UNSPEC)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use OMIT/KEEP QUOTES clause for scalar columns"
+ " without also specifying FORMAT clause"),
+ parser_errposition(pstate, rawc->location)));
+
+ /* FALLTHROUGH */
+ case JTC_EXISTS:
+ case JTC_FORMATTED:
+ {
+ Node *je;
+ CaseTestExpr *param = makeNode(CaseTestExpr);
+
+ param->collation = InvalidOid;
+ param->typeId = cxt->contextItemTypid;
+ param->typeMod = -1;
+
+ if (rawc->wrapper != JSW_NONE &&
+ rawc->quotes != JS_QUOTES_UNSPEC)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot use WITH WRAPPER clause for formatted colunmns"
+ " without also specifying OMIT/KEEP QUOTES"),
+ parser_errposition(pstate, rawc->location)));
+
+ je = transformJsonTableColumn(rawc, (Node *) param,
+ NIL, errorOnError);
+
+ colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION);
+ assign_expr_collations(pstate, colexpr);
+
+ typid = exprType(colexpr);
+ typmod = exprTypmod(colexpr);
+ break;
+ }
+
+ case JTC_NESTED:
+ continue;
+
+ default:
+ elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype);
+ break;
+ }
+
+ tf->coltypes = lappend_oid(tf->coltypes, typid);
+ tf->coltypmods = lappend_int(tf->coltypmods, typmod);
+ tf->colcollations = lappend_oid(tf->colcollations, get_typcollation(typid));
+ tf->colvalexprs = lappend(tf->colvalexprs, colexpr);
+ }
+}
+
+/*
+ * 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.
+ */
+static JsonTableParent *
+makeParentJsonTableNode(JsonTableParseContext * cxt, char *pathSpec, char *pathName,
+ List *columns)
+{
+ JsonTableParent *node = makeNode(JsonTableParent);
+ JsonBehavior *on_error = cxt->table->behavior != NIL ?
+ lsecond(cxt->table->behavior) : NULL;
+
+ node->path = makeNode(JsonTablePath);
+ node->path->value = makeConst(JSONPATHOID, -1, InvalidOid, -1,
+ DirectFunctionCall1(jsonpath_in,
+ CStringGetDatum(pathSpec)),
+ false, false);
+ if (pathName)
+ node->path->name = pstrdup(pathName);
+
+ /* save start of column range */
+ node->colMin = list_length(cxt->tablefunc->colvalexprs);
+
+ appendJsonTableColumns(cxt, columns);
+
+ /* save end of column range */
+ node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1;
+
+ node->errorOnError = on_error && on_error->btype == JSON_BEHAVIOR_ERROR;
+
+ return node;
+}
+
+static JsonTableParent *
+transformJsonTableColumns(JsonTableParseContext * cxt, JsonTablePlan *plan,
+ List *columns, char *pathSpec, char **pathName,
+ int location)
+{
+ JsonTableParent *node;
+ JsonTablePlan *childPlan;
+ bool defaultPlan = !plan || plan->plan_type == JSTP_DEFAULT;
+
+ if (!*pathName)
+ {
+ if (cxt->table->plan)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE expression"),
+ errdetail("JSON_TABLE columns must contain "
+ "explicit AS pathname specification if "
+ "explicit PLAN clause is used"),
+ parser_errposition(cxt->pstate, location)));
+
+ *pathName = generateJsonTablePathName(cxt);
+ }
+
+ if (defaultPlan)
+ childPlan = plan;
+ else
+ {
+ /* validate parent and child plans */
+ JsonTablePlan *parentPlan;
+
+ if (plan->plan_type == JSTP_JOINED)
+ {
+ if (plan->join_type != JSTPJ_INNER &&
+ plan->join_type != JSTPJ_OUTER)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE plan"),
+ errdetail("Expected INNER or OUTER JSON_TABLE plan node."),
+ parser_errposition(cxt->pstate, plan->location)));
+
+ parentPlan = plan->plan1;
+ childPlan = plan->plan2;
+
+ Assert(parentPlan->plan_type != JSTP_JOINED);
+ Assert(parentPlan->pathname);
+ }
+ else
+ {
+ parentPlan = plan;
+ childPlan = NULL;
+ }
+
+ if (strcmp(parentPlan->pathname, *pathName))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("invalid JSON_TABLE plan"),
+ errdetail("Path name mismatch: expected %s but %s is given.",
+ *pathName, parentPlan->pathname),
+ parser_errposition(cxt->pstate, plan->location)));
+
+ validateJsonTableChildPlan(cxt->pstate, childPlan, columns);
+ }
+
+ /* transform only non-nested columns */
+ node = makeParentJsonTableNode(cxt, pathSpec, *pathName, columns);
+
+ if (childPlan || defaultPlan)
+ {
+ /* transform recursively nested columns */
+ node->child = transformJsonTableChildPlan(cxt, childPlan, columns);
+ if (node->child)
+ node->outerJoin = !plan || (plan->join_type & JSTPJ_OUTER);
+ /* else: default plan case, no children found */
+ }
+
+ return node;
+}
+
+/*
+ * transformJsonTable -
+ * Transform a raw JsonTable into TableFunc.
+ *
+ * Transform the document-generating expression, the row-generating expression,
+ * the column-generating expressions, and the default value expressions.
+ */
+ParseNamespaceItem *
+transformJsonTable(ParseState *pstate, JsonTable *jt)
+{
+ JsonTableParseContext cxt;
+ TableFunc *tf = makeNode(TableFunc);
+ JsonFuncExpr *jfe = makeNode(JsonFuncExpr);
+ JsonExpr *je;
+ JsonTablePlan *plan = jt->plan;
+ char *rootPathName = jt->pathname;
+ char *rootPath;
+ bool is_lateral;
+
+ cxt.pstate = pstate;
+ cxt.table = jt;
+ cxt.tablefunc = tf;
+ cxt.pathNames = NIL;
+ cxt.pathNameId = 0;
+
+ if (rootPathName)
+ registerJsonTableColumn(&cxt, rootPathName);
+
+ registerAllJsonTableColumns(&cxt, jt->columns);
+
+#if 0 /* XXX it' unclear from the standard whether
+ * root path name is mandatory or not */
+ if (plan && plan->plan_type != JSTP_DEFAULT && !rootPathName)
+ {
+ /* Assign root path name and create corresponding plan node */
+ JsonTablePlan *rootNode = makeNode(JsonTablePlan);
+ JsonTablePlan *rootPlan = (JsonTablePlan *)
+ makeJsonTableJoinedPlan(JSTPJ_OUTER, (Node *) rootNode,
+ (Node *) plan, jt->location);
+
+ rootPathName = generateJsonTablePathName(&cxt);
+
+ rootNode->plan_type = JSTP_SIMPLE;
+ rootNode->pathname = rootPathName;
+
+ plan = rootPlan;
+ }
+#endif
+
+ jfe->op = JSON_TABLE_OP;
+ jfe->context_item = jt->context_item;
+ jfe->pathspec = jt->pathspec;
+ jfe->pathname = jt->pathname;
+ jfe->passing = jt->passing;
+ jfe->behavior = jt->behavior;
+ jfe->location = jt->location;
+
+ /*
+ * We make lateral_only names of this level visible, whether or not the
+ * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL
+ * spec compliance and seems useful on convenience grounds for all
+ * functions in FROM.
+ *
+ * (LATERAL can't nest within a single pstate level, so we don't need
+ * save/restore logic here.)
+ */
+ Assert(!pstate->p_lateral_active);
+ pstate->p_lateral_active = true;
+
+ tf->functype = TFT_JSON_TABLE;
+ tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION);
+
+ cxt.contextItemTypid = exprType(tf->docexpr);
+
+ if (!IsA(jt->pathspec, A_Const) ||
+ castNode(A_Const, jt->pathspec)->val.node.type != T_String)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("only string constants supported in JSON_TABLE path specification"),
+ parser_errposition(pstate,
+ exprLocation(jt->pathspec))));
+
+ rootPath = castNode(A_Const, jt->pathspec)->val.sval.sval;
+
+ tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns,
+ rootPath, &rootPathName,
+ exprLocation(jt->pathspec));
+
+ /* Also save a copy of the PASSING arguments in the TableFunc node. */
+ je = (JsonExpr *) tf->docexpr;
+ tf->passingvalexprs = copyObject(je->passing_values);
+
+ tf->ordinalitycol = -1; /* undefine ordinality column number */
+ tf->location = jt->location;
+
+ pstate->p_lateral_active = false;
+
+ /*
+ * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if
+ * there are any lateral cross-references in it.
+ */
+ is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0);
+
+ return addRangeTableEntryForTableFunc(pstate,
+ tf, jt->alias, is_lateral, true);
+}
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 864ea9b0d5..3ac7e113f0 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2073,7 +2073,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate,
Assert(list_length(tf->coltypmods) == list_length(tf->colnames));
Assert(list_length(tf->colcollations) == list_length(tf->colnames));
- refname = alias ? alias->aliasname : pstrdup("xmltable");
+ refname = alias ? alias->aliasname :
+ pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table");
rte->rtekind = RTE_TABLEFUNC;
rte->relid = InvalidOid;
@@ -2096,7 +2097,7 @@ addRangeTableEntryForTableFunc(ParseState *pstate,
ereport(ERROR,
(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
errmsg("%s function has %d columns available but %d columns specified",
- "XMLTABLE",
+ tf->functype == TFT_XMLTABLE ? "XMLTABLE" : "JSON_TABLE",
list_length(tf->colnames), numaliases)));
rte->eref = eref;
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 8f81624f97..ca7228ea41 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -2002,6 +2002,9 @@ FigureColnameInternal(Node *node, char **name)
case JSON_VALUE_OP:
*name = "json_value";
return 2;
+ case JSON_TABLE_OP:
+ *name = "json_table";
+ return 2;
}
break;
default:
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 6c5602c64d..bb559d033f 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -61,10 +61,12 @@
#include "catalog/pg_collation.h"
#include "catalog/pg_type.h"
+#include "executor/execExpr.h"
#include "funcapi.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
#include "nodes/miscnodes.h"
+#include "nodes/nodeFuncs.h"
#include "regex/regex.h"
#include "utils/builtins.h"
#include "utils/date.h"
@@ -75,6 +77,8 @@
#include "utils/guc.h"
#include "utils/json.h"
#include "utils/jsonpath.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
@@ -156,6 +160,61 @@ typedef struct JsonValueListIterator
ListCell *next;
} JsonValueListIterator;
+/* Structures for JSON_TABLE execution */
+
+typedef enum JsonTablePlanStateType
+{
+ JSON_TABLE_SCAN_STATE = 0,
+ JSON_TABLE_JOIN_STATE,
+} JsonTablePlanStateType;
+
+typedef struct JsonTablePlanState
+{
+ JsonTablePlanStateType type;
+
+ struct JsonTablePlanState *parent;
+ struct JsonTablePlanState *nested;
+} JsonTablePlanState;
+
+typedef struct JsonTableScanState
+{
+ JsonTablePlanState plan;
+
+ MemoryContext mcxt;
+ JsonPath *path;
+ List *args;
+ JsonValueList found;
+ JsonValueListIterator iter;
+ Datum current;
+ int ordinal;
+ bool currentIsNull;
+ bool outerJoin;
+ bool errorOnError;
+ bool advanceNested;
+ bool reset;
+} JsonTableScanState;
+
+typedef struct JsonTableJoinState
+{
+ JsonTablePlanState plan;
+
+ JsonTablePlanState *left;
+ JsonTablePlanState *right;
+ bool cross;
+ bool advanceRight;
+} JsonTableJoinState;
+
+/* random number to identify JsonTableExecContext */
+#define JSON_TABLE_EXEC_CONTEXT_MAGIC 418352867
+
+typedef struct JsonTableExecContext
+{
+ int magic;
+ JsonTableScanState **colexprscans;
+ JsonTableScanState *root;
+ bool empty;
+} JsonTableExecContext;
+
/* strict/lax flags is decomposed into four [un]wrap/error flags */
#define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode)
#define jspAutoUnwrap(cxt) ((cxt)->laxMode)
@@ -248,6 +307,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt,
JsonPathItem *jsp, JsonbValue *jb, int32 *index);
static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt,
JsonbValue *jbv, int32 id);
+static void JsonValueListClear(JsonValueList *jvl);
static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv);
static int JsonValueListLength(const JsonValueList *jvl);
static bool JsonValueListIsEmpty(JsonValueList *jvl);
@@ -265,6 +325,14 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items);
static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2,
bool useTz, bool *cast_error);
+
+static JsonTablePlanState * JsonTableInitPlanState(JsonTableExecContext * cxt,
+ Node *plan,
+ JsonTablePlanState * parent);
+static bool JsonTablePlanNextRow(JsonTablePlanState * state);
+static bool JsonTableScanNextRow(JsonTableScanState *scan);
+
+
/****************** User interface to JsonPath executor ********************/
/*
@@ -2518,6 +2586,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id)
return baseObject;
}
+static void
+JsonValueListClear(JsonValueList *jvl)
+{
+ jvl->singleton = NULL;
+ jvl->list = NULL;
+}
+
static void
JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv)
{
@@ -3123,3 +3198,471 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res)
errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types")));
}
}
+
+/************************ JSON_TABLE functions ***************************/
+
+/*
+ * Returns private data from executor state. Ensure validity by check with
+ * MAGIC number.
+ */
+static inline JsonTableExecContext *
+GetJsonTableExecContext(TableFuncScanState *state, const char *fname)
+{
+ JsonTableExecContext *result;
+
+ if (!IsA(state, TableFuncScanState))
+ elog(ERROR, "%s called with invalid TableFuncScanState", fname);
+ result = (JsonTableExecContext *) state->opaque;
+ if (result->magic != JSON_TABLE_EXEC_CONTEXT_MAGIC)
+ elog(ERROR, "%s called with invalid TableFuncScanState", fname);
+
+ return result;
+}
+
+/* Recursively initialize JSON_TABLE scan / join state */
+static JsonTableJoinState *
+JsonTableInitJoinState(JsonTableExecContext * cxt, JsonTableSibling *plan,
+ JsonTablePlanState * parent)
+{
+ JsonTableJoinState *join = palloc0(sizeof(*join));
+
+ join->plan.type = JSON_TABLE_JOIN_STATE;
+ /* parent and nested not set. */
+
+ join->cross = plan->cross;
+ join->left = JsonTableInitPlanState(cxt, plan->larg, parent);
+ join->right = JsonTableInitPlanState(cxt, plan->rarg, parent);
+
+ return join;
+}
+
+static JsonTableScanState *
+JsonTableInitScanState(JsonTableExecContext * cxt, JsonTableParent *plan,
+ JsonTablePlanState * parent,
+ List *args, MemoryContext mcxt)
+{
+ JsonTableScanState *scan = palloc0(sizeof(*scan));
+ int i;
+
+ scan->plan.type = JSON_TABLE_SCAN_STATE;
+ scan->plan.parent = parent;
+
+ scan->outerJoin = plan->outerJoin;
+ scan->errorOnError = plan->errorOnError;
+ scan->path = DatumGetJsonPathP(plan->path->value->constvalue);
+ scan->args = args;
+ scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /*
+ * Set after settings scan->args and scan->mcxt, because the recursive
+ * call wants to use those values.
+ */
+ scan->plan.nested = plan->child ?
+ JsonTableInitPlanState(cxt, plan->child, (JsonTablePlanState *) scan) :
+ NULL;
+
+ scan->current = PointerGetDatum(NULL);
+ scan->currentIsNull = true;
+
+ for (i = plan->colMin; i <= plan->colMax; i++)
+ cxt->colexprscans[i] = scan;
+
+ return scan;
+}
+
+/* Recursively initialize JSON_TABLE scan state */
+static JsonTablePlanState *
+JsonTableInitPlanState(JsonTableExecContext * cxt, Node *plan,
+ JsonTablePlanState * parent)
+{
+ JsonTablePlanState *state;
+
+ if (IsA(plan, JsonTableSibling))
+ {
+ JsonTableSibling *join = castNode(JsonTableSibling, plan);
+
+ state = (JsonTablePlanState *)
+ JsonTableInitJoinState(cxt, join, parent);
+ }
+ else
+ {
+ JsonTableParent *scan = castNode(JsonTableParent, plan);
+ JsonTableScanState *parent_scan = (JsonTableScanState *) parent;
+
+ Assert(parent_scan);
+ state = (JsonTablePlanState *)
+ JsonTableInitScanState(cxt, scan, parent, parent_scan->args,
+ parent_scan->mcxt);
+ }
+
+ return state;
+}
+
+/*
+ * JsonTableInitOpaque
+ * Fill in TableFuncScanState->opaque for JsonTable processor
+ */
+static void
+JsonTableInitOpaque(TableFuncScanState *state, int natts)
+{
+ JsonTableExecContext *cxt;
+ PlanState *ps = &state->ss.ps;
+ TableFuncScan *tfs = castNode(TableFuncScan, ps->plan);
+ TableFunc *tf = tfs->tablefunc;
+ JsonTableParent *root = castNode(JsonTableParent, tf->plan);
+ JsonExpr *je = castNode(JsonExpr, tf->docexpr);
+ List *args = NIL;
+
+ cxt = palloc0(sizeof(JsonTableExecContext));
+ cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC;
+
+ if (state->passingvalexprs)
+ {
+ ListCell *exprlc;
+ ListCell *namelc;
+
+ Assert(list_length(state->passingvalexprs) ==
+ list_length(je->passing_names));
+ forboth(exprlc, state->passingvalexprs,
+ namelc, je->passing_names)
+ {
+ ExprState *state = lfirst_node(ExprState, exprlc);
+ String *name = lfirst_node(String, namelc);
+ JsonPathVariable *var = palloc(sizeof(*var));
+
+ var->name = pstrdup(name->sval);
+ var->typid = exprType((Node *) state->expr);
+ var->typmod = exprTypmod((Node *) state->expr);
+
+ /*
+ * Evaluate the expression and save the value to be returned by
+ * GetJsonPathVar().
+ */
+ var->value = ExecEvalExpr(state, ps->ps_ExprContext,
+ &var->isnull);
+
+ args = lappend(args, var);
+ }
+ }
+
+ cxt->colexprscans = palloc(sizeof(JsonTableScanState *) *
+ list_length(tf->colvalexprs));
+
+ cxt->root = JsonTableInitScanState(cxt, root, NULL, args,
+ CurrentMemoryContext);
+ state->opaque = cxt;
+}
+
+/* Reset scan iterator to the beginning of the item list */
+static void
+JsonTableRescan(JsonTableScanState *scan)
+{
+ JsonValueListInitIterator(&scan->found, &scan->iter);
+ scan->current = PointerGetDatum(NULL);
+ scan->currentIsNull = true;
+ scan->advanceNested = false;
+ scan->ordinal = 0;
+}
+
+/* Reset context item of a scan, execute JSON path and reset a scan */
+static void
+JsonTableResetContextItem(JsonTableScanState *scan, Datum item)
+{
+ MemoryContext oldcxt;
+ JsonPathExecResult res;
+ Jsonb *js = (Jsonb *) DatumGetJsonbP(item);
+
+ JsonValueListClear(&scan->found);
+
+ MemoryContextResetOnly(scan->mcxt);
+
+ oldcxt = MemoryContextSwitchTo(scan->mcxt);
+
+ res = executeJsonPath(scan->path, scan->args, GetJsonPathVar, js,
+ scan->errorOnError, &scan->found,
+ false /* FIXME */ );
+
+ MemoryContextSwitchTo(oldcxt);
+
+ if (jperIsError(res))
+ {
+ Assert(!scan->errorOnError);
+ JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */
+ }
+
+ JsonTableRescan(scan);
+}
+
+/*
+ * JsonTableSetDocument
+ * Install the input document
+ */
+static void
+JsonTableSetDocument(TableFuncScanState *state, Datum value)
+{
+ JsonTableExecContext *cxt =
+ GetJsonTableExecContext(state, "JsonTableSetDocument");
+
+ JsonTableResetContextItem(cxt->root, value);
+}
+
+/* Recursively reset scan and its child nodes */
+static void
+JsonTableRescanRecursive(JsonTablePlanState * state)
+{
+ if (state->type == JSON_TABLE_JOIN_STATE)
+ {
+ JsonTableJoinState *join = (JsonTableJoinState *) state;
+
+ JsonTableRescanRecursive(join->left);
+ JsonTableRescanRecursive(join->right);
+ join->advanceRight = false;
+ }
+ else
+ {
+ JsonTableScanState *scan = (JsonTableScanState *) state;
+
+ Assert(state->type == JSON_TABLE_SCAN_STATE);
+ JsonTableRescan(scan);
+ if (scan->plan.nested)
+ JsonTableRescanRecursive(scan->plan.nested);
+ }
+}
+
+/*
+ * Fetch next row from a cross/union joined scan.
+ *
+ * Returns false at the end of a scan, true otherwise.
+ */
+static bool
+JsonTablePlanNextRow(JsonTablePlanState * state)
+{
+ JsonTableJoinState *join;
+
+ if (state->type == JSON_TABLE_SCAN_STATE)
+ return JsonTableScanNextRow((JsonTableScanState *) state);
+
+ join = (JsonTableJoinState *) state;
+ if (join->advanceRight)
+ {
+ /* fetch next inner row */
+ if (JsonTablePlanNextRow(join->right))
+ return true;
+
+ /* inner rows are exhausted */
+ if (join->cross)
+ join->advanceRight = false; /* next outer row */
+ else
+ return false; /* end of scan */
+ }
+
+ while (!join->advanceRight)
+ {
+ /* fetch next outer row */
+ bool left = JsonTablePlanNextRow(join->left);
+
+ if (join->cross)
+ {
+ if (!left)
+ return false; /* end of scan */
+
+ JsonTableRescanRecursive(join->right);
+
+ if (!JsonTablePlanNextRow(join->right))
+ continue; /* next outer row */
+
+ join->advanceRight = true; /* next inner row */
+ }
+ else if (!left)
+ {
+ if (!JsonTablePlanNextRow(join->right))
+ return false; /* end of scan */
+
+ join->advanceRight = true; /* next inner row */
+ }
+
+ break;
+ }
+
+ return true;
+}
+
+/* Recursively set 'reset' flag of scan and its child nodes */
+static void
+JsonTablePlanReset(JsonTablePlanState * state)
+{
+ if (state->type == JSON_TABLE_JOIN_STATE)
+ {
+ JsonTableJoinState *join = (JsonTableJoinState *) state;
+
+ JsonTablePlanReset(join->left);
+ JsonTablePlanReset(join->right);
+ join->advanceRight = false;
+ }
+ else
+ {
+ JsonTableScanState *scan;
+
+ Assert(state->type == JSON_TABLE_SCAN_STATE);
+ scan = (JsonTableScanState *) state;
+ scan->reset = true;
+ scan->advanceNested = false;
+
+ if (scan->plan.nested)
+ JsonTablePlanReset(scan->plan.nested);
+ }
+}
+
+/*
+ * Fetch next row from a simple scan with outer/inner joined nested subscans.
+ *
+ * Returns false at the end of a scan, true otherwise.
+ */
+static bool
+JsonTableScanNextRow(JsonTableScanState *scan)
+{
+ /* reset context item if requested */
+ if (scan->reset)
+ {
+ JsonTableScanState *parent_scan =
+ (JsonTableScanState *) scan->plan.parent;
+
+ Assert(parent_scan && !parent_scan->currentIsNull);
+ JsonTableResetContextItem(scan, parent_scan->current);
+ scan->reset = false;
+ }
+
+ if (scan->advanceNested)
+ {
+ /* fetch next nested row */
+ scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested);
+
+ if (scan->advanceNested)
+ return true;
+ }
+
+ for (;;)
+ {
+ /* fetch next row */
+ JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter);
+ MemoryContext oldcxt;
+
+ if (!jbv)
+ {
+ scan->current = PointerGetDatum(NULL);
+ scan->currentIsNull = true;
+ return false; /* end of scan */
+ }
+
+ /* set current row item */
+ oldcxt = MemoryContextSwitchTo(scan->mcxt);
+ scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv));
+ scan->currentIsNull = false;
+ MemoryContextSwitchTo(oldcxt);
+
+ scan->ordinal++;
+
+ if (!scan->plan.nested)
+ break;
+
+ JsonTablePlanReset(scan->plan.nested);
+
+ scan->advanceNested = JsonTablePlanNextRow(scan->plan.nested);
+
+ if (scan->advanceNested || scan->outerJoin)
+ break;
+ }
+
+ return true;
+}
+
+/*
+ * JsonTableFetchRow
+ * Prepare the next "current" tuple for upcoming GetValue calls.
+ * Returns FALSE if the row-filter expression returned no more rows.
+ */
+static bool
+JsonTableFetchRow(TableFuncScanState *state)
+{
+ JsonTableExecContext *cxt =
+ GetJsonTableExecContext(state, "JsonTableFetchRow");
+
+ if (cxt->empty)
+ return false;
+
+ return JsonTableScanNextRow(cxt->root);
+}
+
+/*
+ * JsonTableGetValue
+ * Return the value for column number 'colnum' for the current row.
+ *
+ * This leaks memory, so be sure to reset often the context in which it's
+ * called.
+ */
+static Datum
+JsonTableGetValue(TableFuncScanState *state, int colnum,
+ Oid typid, int32 typmod, bool *isnull)
+{
+ JsonTableExecContext *cxt =
+ GetJsonTableExecContext(state, "JsonTableGetValue");
+ ExprContext *econtext = state->ss.ps.ps_ExprContext;
+ ExprState *estate = list_nth(state->colvalexprs, colnum);
+ JsonTableScanState *scan = cxt->colexprscans[colnum];
+ Datum result;
+
+ if (scan->currentIsNull) /* NULL from outer/union join */
+ {
+ result = (Datum) 0;
+ *isnull = true;
+ }
+ else if (estate) /* regular column */
+ {
+ Datum saved_caseValue = econtext->caseValue_datum;
+ bool saved_caseIsNull = econtext->caseValue_isNull;
+
+ /* Pass the value for CaseTestExpr that may be present in colexpr */
+ econtext->caseValue_datum = scan->current;
+ econtext->caseValue_isNull = false;
+
+ result = ExecEvalExpr(estate, econtext, isnull);
+
+ econtext->caseValue_datum = saved_caseValue;
+ econtext->caseValue_isNull = saved_caseIsNull;
+ }
+ else
+ {
+ result = Int32GetDatum(scan->ordinal); /* ordinality column */
+ *isnull = false;
+ }
+
+ return result;
+}
+
+/*
+ * JsonTableDestroyOpaque
+ */
+static void
+JsonTableDestroyOpaque(TableFuncScanState *state)
+{
+ JsonTableExecContext *cxt =
+ GetJsonTableExecContext(state, "JsonTableDestroyOpaque");
+
+ /* not valid anymore */
+ cxt->magic = 0;
+
+ state->opaque = NULL;
+}
+
+const TableFuncRoutine JsonbTableRoutine =
+{
+ JsonTableInitOpaque,
+ JsonTableSetDocument,
+ NULL,
+ NULL,
+ NULL,
+ JsonTableFetchRow,
+ JsonTableGetValue,
+ JsonTableDestroyOpaque
+};
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a1745c89ff..46f49c7365 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -522,6 +522,8 @@ static char *flatten_reloptions(Oid relid);
static void get_reloptions(StringInfo buf, Datum reloptions);
static void get_json_path_spec(Node *path_spec, deparse_context *context,
bool showimplicit);
+static void get_json_table_columns(TableFunc *tf, JsonTableParent *node,
+ deparse_context *context, bool showimplicit);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -8627,7 +8629,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context,
/*
* get_json_expr_options
*
- * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS.
+ * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and
+ * JSON_TABLE columns.
*/
static void
get_json_expr_options(JsonExpr *jsexpr, deparse_context *context,
@@ -9874,6 +9877,9 @@ get_rule_expr(Node *node, deparse_context *context,
case JSON_VALUE_OP:
appendStringInfoString(buf, "JSON_VALUE(");
break;
+ default:
+ elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op);
+ break;
}
get_rule_expr(jexpr->formatted_expr, context, showimplicit);
@@ -11240,16 +11246,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context)
/* ----------
- * get_tablefunc - Parse back a table function
+ * get_xmltable - Parse back a XMLTABLE function
* ----------
*/
static void
-get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit)
+get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit)
{
StringInfo buf = context->buf;
- /* XMLTABLE is the only existing implementation. */
-
appendStringInfoString(buf, "XMLTABLE(");
if (tf->ns_uris != NIL)
@@ -11340,6 +11344,271 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit)
appendStringInfoChar(buf, ')');
}
+/*
+ * get_json_nested_columns - Parse back nested JSON_TABLE columns
+ */
+static void
+get_json_table_nested_columns(TableFunc *tf, Node *node,
+ deparse_context *context, bool showimplicit,
+ bool needcomma)
+{
+ if (IsA(node, JsonTableSibling))
+ {
+ JsonTableSibling *n = (JsonTableSibling *) node;
+
+ get_json_table_nested_columns(tf, n->larg, context, showimplicit,
+ needcomma);
+ get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true);
+ }
+ else
+ {
+ JsonTableParent *n = castNode(JsonTableParent, node);
+
+ if (needcomma)
+ appendStringInfoChar(context->buf, ',');
+
+ appendStringInfoChar(context->buf, ' ');
+ appendContextKeyword(context, "NESTED PATH ", 0, 0, 0);
+ get_const_expr(n->path->value, context, -1);
+ appendStringInfo(context->buf, " AS %s", quote_identifier(n->path->name));
+ get_json_table_columns(tf, n, context, showimplicit);
+ }
+}
+
+/*
+ * get_json_table_plan - Parse back a JSON_TABLE plan
+ */
+static void
+get_json_table_plan(TableFunc *tf, Node *node, deparse_context *context,
+ bool parenthesize)
+{
+ if (parenthesize)
+ appendStringInfoChar(context->buf, '(');
+
+ if (IsA(node, JsonTableSibling))
+ {
+ JsonTableSibling *n = (JsonTableSibling *) node;
+
+ get_json_table_plan(tf, n->larg, context,
+ IsA(n->larg, JsonTableSibling) ||
+ castNode(JsonTableParent, n->larg)->child);
+
+ appendStringInfoString(context->buf, n->cross ? " CROSS " : " UNION ");
+
+ get_json_table_plan(tf, n->rarg, context,
+ IsA(n->rarg, JsonTableSibling) ||
+ castNode(JsonTableParent, n->rarg)->child);
+ }
+ else
+ {
+ JsonTableParent *n = castNode(JsonTableParent, node);
+
+ appendStringInfoString(context->buf, quote_identifier(n->path->name));
+
+ if (n->child)
+ {
+ appendStringInfoString(context->buf,
+ n->outerJoin ? " OUTER " : " INNER ");
+ get_json_table_plan(tf, n->child, context,
+ IsA(n->child, JsonTableSibling));
+ }
+ }
+
+ if (parenthesize)
+ appendStringInfoChar(context->buf, ')');
+}
+
+/*
+ * get_json_table_columns - Parse back JSON_TABLE columns
+ */
+static void
+get_json_table_columns(TableFunc *tf, JsonTableParent *node,
+ deparse_context *context, bool showimplicit)
+{
+ StringInfo buf = context->buf;
+ JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr);
+ ListCell *lc_colname;
+ ListCell *lc_coltype;
+ ListCell *lc_coltypmod;
+ ListCell *lc_colvalexpr;
+ int colnum = 0;
+
+ appendStringInfoChar(buf, ' ');
+ appendContextKeyword(context, "COLUMNS (", 0, 0, 0);
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel += PRETTYINDENT_VAR;
+
+ forfour(lc_colname, tf->colnames,
+ lc_coltype, tf->coltypes,
+ lc_coltypmod, tf->coltypmods,
+ lc_colvalexpr, tf->colvalexprs)
+ {
+ char *colname = strVal(lfirst(lc_colname));
+ JsonExpr *colexpr;
+ Oid typid;
+ int32 typmod;
+ bool ordinality;
+ JsonBehaviorType default_behavior;
+
+ typid = lfirst_oid(lc_coltype);
+ typmod = lfirst_int(lc_coltypmod);
+ colexpr = castNode(JsonExpr, lfirst(lc_colvalexpr));
+
+ if (colnum < node->colMin)
+ {
+ colnum++;
+ continue;
+ }
+
+ if (colnum > node->colMax)
+ break;
+
+ if (colnum > node->colMin)
+ appendStringInfoString(buf, ", ");
+
+ colnum++;
+
+ ordinality = !colexpr;
+
+ appendContextKeyword(context, "", 0, 0, 0);
+
+ appendStringInfo(buf, "%s %s", quote_identifier(colname),
+ ordinality ? "FOR ORDINALITY" :
+ format_type_with_typemod(typid, typmod));
+ if (ordinality)
+ continue;
+
+ if (colexpr->op == JSON_EXISTS_OP)
+ {
+ appendStringInfoString(buf, " EXISTS");
+ default_behavior = JSON_BEHAVIOR_FALSE;
+ }
+ else
+ {
+ if (colexpr->op == JSON_QUERY_OP)
+ {
+ char typcategory;
+ bool typispreferred;
+
+ get_type_category_preferred(typid, &typcategory, &typispreferred);
+
+ if (typcategory == TYPCATEGORY_STRING)
+ appendStringInfoString(buf,
+ colexpr->format->format_type == JS_FORMAT_JSONB ?
+ " FORMAT JSONB" : " FORMAT JSON");
+ }
+
+ 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);
+
+ get_json_expr_options(colexpr, context, default_behavior);
+ }
+
+ if (node->child)
+ get_json_table_nested_columns(tf, node->child, context, showimplicit,
+ node->colMax >= node->colMin);
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel -= PRETTYINDENT_VAR;
+
+ appendContextKeyword(context, ")", 0, 0, 0);
+}
+
+/* ----------
+ * get_json_table - Parse back a JSON_TABLE function
+ * ----------
+ */
+static void
+get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit)
+{
+ StringInfo buf = context->buf;
+ JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr);
+ JsonTableParent *root = castNode(JsonTableParent, tf->plan);
+
+ appendStringInfoString(buf, "JSON_TABLE(");
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel += PRETTYINDENT_VAR;
+
+ appendContextKeyword(context, "", 0, 0, 0);
+
+ get_rule_expr(jexpr->formatted_expr, context, showimplicit);
+
+ appendStringInfoString(buf, ", ");
+
+ get_const_expr(root->path->value, context, -1);
+
+ appendStringInfo(buf, " AS %s", quote_identifier(root->path->name));
+
+ if (jexpr->passing_values)
+ {
+ ListCell *lc1,
+ *lc2;
+ bool needcomma = false;
+
+ appendStringInfoChar(buf, ' ');
+ appendContextKeyword(context, "PASSING ", 0, 0, 0);
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel += PRETTYINDENT_VAR;
+
+ forboth(lc1, jexpr->passing_names,
+ lc2, jexpr->passing_values)
+ {
+ if (needcomma)
+ appendStringInfoString(buf, ", ");
+ needcomma = true;
+
+ appendContextKeyword(context, "", 0, 0, 0);
+
+ get_rule_expr((Node *) lfirst(lc2), context, false);
+ appendStringInfo(buf, " AS %s",
+ quote_identifier((lfirst_node(String, lc1))->sval)
+ );
+ }
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel -= PRETTYINDENT_VAR;
+ }
+
+ get_json_table_columns(tf, root, context, showimplicit);
+
+ appendStringInfoChar(buf, ' ');
+ appendContextKeyword(context, "PLAN ", 0, 0, 0);
+ get_json_table_plan(tf, (Node *) root, context, true);
+
+ if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY)
+ get_json_behavior(jexpr->on_error, context, "ERROR");
+
+ if (PRETTY_INDENT(context))
+ context->indentLevel -= PRETTYINDENT_VAR;
+
+ appendContextKeyword(context, ")", 0, 0, 0);
+}
+
+/* ----------
+ * get_tablefunc - Parse back a table function
+ * ----------
+ */
+static void
+get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit)
+{
+ /* XMLTABLE and JSON_TABLE are the only existing implementations. */
+
+ if (tf->functype == TFT_XMLTABLE)
+ get_xmltable(tf, context, showimplicit);
+ else if (tf->functype == TFT_JSON_TABLE)
+ get_json_table(tf, context, showimplicit);
+}
+
/* ----------
* get_from_clause - Parse back a FROM clause
*
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6a7118d300..2fa0328977 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1882,6 +1882,8 @@ typedef struct TableFuncScanState
ExprState *rowexpr; /* state for row-generating expression */
List *colexprs; /* state for column-generating expression */
List *coldefexprs; /* state for column default expressions */
+ List *colvalexprs; /* state for column value expression */
+ List *passingvalexprs; /* state for PASSING argument expression */
List *ns_names; /* same as TableFunc.ns_names */
List *ns_uris; /* list of states of namespace URI exprs */
Bitmapset *notnulls; /* nullability flag for each output column */
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index f98578c92e..50fc639365 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -114,6 +114,8 @@ extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr,
JsonFormat *format);
extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr,
JsonCoercion *coercion, int location);
+extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type,
+ Node *plan1, Node *plan2, int location);
extern Node *makeJsonKeyValue(Node *key, Node *value);
extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format,
JsonValueType item_type, bool unique_keys,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f0560c3737..a0bcca36d8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1709,6 +1709,19 @@ typedef enum JsonQuotes
*/
typedef char *JsonPathSpec;
+/*
+ * JsonTableColumnType -
+ * enumeration of JSON_TABLE column types
+ */
+typedef enum JsonTableColumnType
+{
+ JTC_FOR_ORDINALITY,
+ JTC_REGULAR,
+ JTC_EXISTS,
+ JTC_FORMATTED,
+ JTC_NESTED,
+} JsonTableColumnType;
+
/*
* JsonOutput -
* representation of JSON output clause (RETURNING type [FORMAT format])
@@ -1741,6 +1754,7 @@ typedef struct JsonFuncExpr
JsonExprOp op; /* expression type */
JsonValueExpr *context_item; /* context item expression */
Node *pathspec; /* JSON path specification expression */
+ char *pathname; /* path name, if any */
List *passing; /* list of PASSING clause arguments, if any */
JsonOutput *output; /* output clause, if specified */
List *behavior; /* ON EMPTY / ERROR behaviors, list of two
@@ -1750,6 +1764,87 @@ typedef struct JsonFuncExpr
int location; /* token location, or -1 if unknown */
} JsonFuncExpr;
+/*
+ * JsonTableColumn -
+ * untransformed representation of JSON_TABLE column
+ */
+typedef struct JsonTableColumn
+{
+ NodeTag type;
+ JsonTableColumnType coltype; /* column type */
+ char *name; /* column name */
+ TypeName *typeName; /* column type name */
+ char *pathspec; /* path specification, if any */
+ char *pathname; /* path name, if any */
+ JsonFormat *format; /* JSON format clause, if specified */
+ JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */
+ JsonQuotes quotes; /* omit or keep quotes on scalar strings? */
+ List *columns; /* nested columns */
+ List *behavior; /* ON EMPTY / ERROR behaviors, list of two
+ * JsonBehavior nodes in that order */
+ int location; /* token location, or -1 if unknown */
+} JsonTableColumn;
+
+/*
+ * JsonTablePlanType -
+ * flags for JSON_TABLE plan node types representation
+ */
+typedef enum JsonTablePlanType
+{
+ JSTP_DEFAULT,
+ JSTP_SIMPLE,
+ JSTP_JOINED,
+} JsonTablePlanType;
+
+/*
+ * JsonTablePlanJoinType -
+ * flags for JSON_TABLE join types representation
+ */
+typedef enum JsonTablePlanJoinType
+{
+ JSTPJ_INNER = 0x01,
+ JSTPJ_OUTER = 0x02,
+ JSTPJ_CROSS = 0x04,
+ JSTPJ_UNION = 0x08,
+} JsonTablePlanJoinType;
+
+typedef struct JsonTablePlan JsonTablePlan;
+
+/*
+ * JsonTablePlan -
+ * untransformed representation of JSON_TABLE plan node
+ */
+struct JsonTablePlan
+{
+ NodeTag type;
+ JsonTablePlanType plan_type; /* plan type */
+ JsonTablePlanJoinType join_type; /* join type (for joined plan only) */
+ JsonTablePlan *plan1; /* first joined plan */
+ JsonTablePlan *plan2; /* second joined plan */
+ char *pathname; /* path name (for simple plan only) */
+ int location; /* token location, or -1 if unknown */
+};
+
+/*
+ * JsonTable -
+ * untransformed representation of JSON_TABLE
+ */
+typedef struct JsonTable
+{
+ NodeTag type;
+ JsonValueExpr *context_item; /* context item expression */
+ Node *pathspec; /* JSON path specification expression */
+ char *pathname; /* path name, if any */
+ List *passing; /* list of PASSING clause arguments, if any */
+ List *columns; /* list of JsonTableColumn */
+ JsonTablePlan *plan; /* join plan, if specified */
+ List *behavior; /* ON EMPTY / ERROR behaviors, list of two
+ * JsonBehavior nodes in that order */
+ Alias *alias; /* table alias in FROM clause */
+ bool lateral; /* does it have LATERAL prefix? */
+ int location; /* token location, or -1 if unknown */
+} JsonTable;
+
/*
* JsonKeyValue -
* untransformed representation of JSON object key-value pair for
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7504b09ca8..75bbd14a35 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -94,8 +94,14 @@ typedef struct RangeVar
int location;
} RangeVar;
+typedef enum TableFuncType
+{
+ TFT_XMLTABLE,
+ TFT_JSON_TABLE,
+} TableFuncType;
+
/*
- * TableFunc - node for a table function, such as XMLTABLE.
+ * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE.
*
* Entries in the ns_names list are either String nodes containing
* literal namespace names, or NULL pointers to represent DEFAULT.
@@ -103,6 +109,8 @@ typedef struct RangeVar
typedef struct TableFunc
{
NodeTag type;
+ /* XMLTABLE or JSON_TABLE */
+ TableFuncType functype;
/* list of namespace URI expressions */
List *ns_uris pg_node_attr(query_jumble_ignore);
/* list of namespace names or NULL */
@@ -123,8 +131,14 @@ typedef struct TableFunc
List *colexprs;
/* list of column default expressions */
List *coldefexprs pg_node_attr(query_jumble_ignore);
+ /* list of column value expressions */
+ List *colvalexprs pg_node_attr(query_jumble_ignore);
+ /* list of PASSING argument expressions */
+ List *passingvalexprs pg_node_attr(query_jumble_ignore);
/* nullability flag for each output column */
Bitmapset *notnulls pg_node_attr(query_jumble_ignore);
+ /* JSON_TABLE plan */
+ Node *plan pg_node_attr(query_jumble_ignore);
/* counts from 0; -1 if none specified */
int ordinalitycol pg_node_attr(query_jumble_ignore);
/* token location, or -1 if unknown */
@@ -1561,6 +1575,7 @@ typedef enum JsonExprOp
JSON_VALUE_OP, /* JSON_VALUE() */
JSON_QUERY_OP, /* JSON_QUERY() */
JSON_EXISTS_OP, /* JSON_EXISTS() */
+ JSON_TABLE_OP, /* JSON_TABLE() */
} JsonExprOp;
/*
@@ -1800,6 +1815,48 @@ typedef struct JsonExpr
int location; /* token location, or -1 if unknown */
} JsonExpr;
+/*
+ * JsonTablePath
+ * A JSON path expression to be computed as part of evaluating
+ * a JSON_TABLE plan node
+ */
+typedef struct JsonTablePath
+{
+ NodeTag type;
+
+ Const *value;
+ char *name;
+} JsonTablePath;
+
+/*
+ * JsonTableParent -
+ * transformed representation of parent JSON_TABLE plan node
+ */
+typedef struct JsonTableParent
+{
+ NodeTag type;
+ JsonTablePath *path;
+ Node *child; /* nested columns, if any */
+ bool outerJoin; /* outer or inner join for nested columns? */
+ int colMin; /* min column index in the resulting column
+ * list */
+ int colMax; /* max column index in the resulting column
+ * list */
+ bool errorOnError; /* ERROR/EMPTY ON ERROR behavior */
+} JsonTableParent;
+
+/*
+ * JsonTableSibling -
+ * transformed representation of joined sibling JSON_TABLE plan node
+ */
+typedef struct JsonTableSibling
+{
+ NodeTag type;
+ Node *larg; /* left join node */
+ Node *rarg; /* right join node */
+ bool cross; /* cross or union join? */
+} JsonTableSibling;
+
/* ----------------
* NullTest
*
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 0954d9fc7b..f88a6c9ac6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -242,6 +242,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -284,6 +285,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -334,7 +336,9 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 8e26482db4..7152a8ef48 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle,
extern Index assignSortGroupRef(TargetEntry *tle, List *tlist);
extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList);
+/* functions in parse_jsontable.c */
+extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt);
+
#endif /* PARSE_CLAUSE_H */
diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h
index 5a37133847..7eb0ccf4e4 100644
--- a/src/include/utils/jsonpath.h
+++ b/src/include/utils/jsonpath.h
@@ -15,6 +15,7 @@
#define JSONPATH_H
#include "fmgr.h"
+#include "executor/tablefunc.h"
#include "nodes/pg_list.h"
#include "nodes/primnodes.h"
#include "utils/jsonb.h"
@@ -288,4 +289,6 @@ extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper,
extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty,
bool *error, List *vars);
+extern PGDLLIMPORT const TableFuncRoutine JsonbTableRoutine;
+
#endif
diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out
index 04d5cc74e3..5d0c6b6fdd 100644
--- a/src/test/regress/expected/json_sqljson.out
+++ b/src/test/regress/expected/json_sqljson.out
@@ -16,3 +16,9 @@ ERROR: JSON_QUERY() is not yet implemented for the json type
LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$');
^
HINT: Try casting the argument to jsonb
+-- JSON_TABLE
+SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text));
+ERROR: JSON_TABLE() is not yet implemented for the json type
+LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ...
+ ^
+HINT: Try casting the argument to jsonb
diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out
index cb727e930a..8fd2385cdc 100644
--- a/src/test/regress/expected/jsonb_sqljson.out
+++ b/src/test/regress/expected/jsonb_sqljson.out
@@ -1030,3 +1030,1185 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti
ERROR: functions in index expression must be marked IMMUTABLE
CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x));
DROP TABLE test_jsonb_mutability;
+-- JSON_TABLE
+-- Should fail (JSON_TABLE can be used only in FROM clause)
+SELECT JSON_TABLE('[]', '$');
+ERROR: syntax error at or near "("
+LINE 1: SELECT JSON_TABLE('[]', '$');
+ ^
+-- Should fail (no columns)
+SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ());
+ERROR: syntax error at or near ")"
+LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ());
+ ^
+SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2);
+ERROR: JSON_TABLE function has 1 columns available but 2 columns specified
+-- NULL => empty table
+SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar;
+ foo
+-----
+(0 rows)
+
+--
+SELECT * FROM JSON_TABLE(jsonb '123', '$'
+ COLUMNS (item int PATH '$', foo int)) bar;
+ item | foo
+------+-----
+ 123 |
+(1 row)
+
+-- JSON_TABLE: basic functionality
+CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo');
+CREATE TEMP TABLE json_table_test (js) AS
+ (VALUES
+ ('1'),
+ ('[]'),
+ ('{}'),
+ ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]')
+ );
+-- Regular "unformatted" columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ "int" int PATH '$',
+ "text" text PATH '$',
+ "char(4)" char(4) PATH '$',
+ "bool" bool PATH '$',
+ "numeric" numeric PATH '$',
+ "domain" jsonb_test_domain PATH '$',
+ js json PATH '$',
+ jb jsonb PATH '$'
+ )
+ ) jt
+ ON true;
+ js | id | int | text | char(4) | bool | numeric | domain | js | jb
+---------------------------------------------------------------------------------------+----+-----+---------+---------+------+---------+---------+--------------+--------------
+ 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1
+ [] | | | | | | | | |
+ {} | 1 | | | | | | | {} | {}
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2"
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa"
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | | foo | foo | | | | "foo" | "foo"
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | | | | | | | null | null
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 0 | false | fals | f | | false | false | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 1 | true | true | t | | true | true | true
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | | | | | | | {"aaa": 123} | {"aaa": 123}
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]"
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\""
+(14 rows)
+
+-- "formatted" columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ jst text FORMAT JSON PATH '$',
+ jsc char(4) FORMAT JSON PATH '$',
+ jsv varchar(4) FORMAT JSON PATH '$',
+ jsb jsonb FORMAT JSON PATH '$',
+ jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES
+ )
+ ) jt
+ ON true;
+ js | id | jst | jsc | jsv | jsb | jsbq
+---------------------------------------------------------------------------------------+----+--------------+------+------+--------------+--------------
+ 1 | 1 | 1 | 1 | 1 | 1 | 1
+ [] | | | | | |
+ {} | 1 | {} | {} | {} | {} | {}
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | 1
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | "2" | "2" | "2" | "2" | 2
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | "foo" | "foo | "foo | "foo" |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | null | null | null | null | null
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | false | fals | fals | false | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | true | true | true | true | true
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123}
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2]
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | "\"str\"" | "\"s | "\"s | "\"str\"" | "str"
+(14 rows)
+
+-- EXISTS columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ exists1 bool EXISTS PATH '$.aaa',
+ exists2 int EXISTS PATH '$.aaa',
+ exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR,
+ exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR
+ )
+ ) jt
+ ON true;
+ js | id | exists1 | exists2 | exists3 | exists4
+---------------------------------------------------------------------------------------+----+---------+---------+---------+---------
+ 1 | 1 | f | 0 | | false
+ [] | | | | |
+ {} | 1 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | t | 1 | 1 | true
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | f | 0 | | false
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | f | 0 | | false
+(14 rows)
+
+-- Other miscellanous checks
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ aaa int, -- "aaa" has implicit path '$."aaa"'
+ aaa1 int PATH '$.aaa',
+ js2 json PATH '$',
+ jsb2w jsonb PATH '$' WITH WRAPPER,
+ jsb2q jsonb PATH '$' OMIT QUOTES,
+ ia int[] PATH '$',
+ ta text[] PATH '$',
+ jba jsonb[] PATH '$'
+ )
+ ) jt
+ ON true;
+ js | id | aaa | aaa1 | js2 | jsb2w | jsb2q | ia | ta | jba
+---------------------------------------------------------------------------------------+----+-----+------+--------------+----------------+--------------+----+----+-----
+ 1 | 1 | | | 1 | [1] | 1 | | |
+ [] | | | | | | | | |
+ {} | 1 | | | {} | [{}] | {} | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | | | 1 | [1] | 1 | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | | | 1.23 | [1.23] | 1.23 | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | | | "2" | ["2"] | 2 | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | | | "aaaaaaa" | ["aaaaaaa"] | | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | | | "foo" | ["foo"] | | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | | | null | [null] | null | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | | | false | [false] | false | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | | | true | [true] | true | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 123 | 123 | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | | | "[1,2]" | ["[1,2]"] | [1, 2] | | |
+ [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | | | "\"str\"" | ["\"str\""] | "str" | | |
+(14 rows)
+
+-- JSON_TABLE: Test backward parsing
+CREATE VIEW jsonb_table_view AS
+SELECT * FROM
+ JSON_TABLE(
+ jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c"
+ COLUMNS (
+ id FOR ORDINALITY,
+ "int" int PATH '$',
+ "text" text PATH '$',
+ "char(4)" char(4) PATH '$',
+ "bool" bool PATH '$',
+ "numeric" numeric PATH '$',
+ "domain" jsonb_test_domain PATH '$',
+ js json PATH '$',
+ jb jsonb PATH '$',
+ jst text FORMAT JSON PATH '$',
+ jsc char(4) FORMAT JSON PATH '$',
+ jsv varchar(4) FORMAT JSON PATH '$',
+ jsb jsonb FORMAT JSON PATH '$',
+ jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES,
+ aaa int, -- implicit path '$."aaa"',
+ aaa1 int PATH '$.aaa',
+ exists1 bool EXISTS PATH '$.aaa',
+ exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR,
+ exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR,
+ js2 json PATH '$',
+ jsb2w jsonb PATH '$' WITH WRAPPER,
+ jsb2q jsonb PATH '$' OMIT QUOTES,
+ ia int[] PATH '$',
+ ta text[] PATH '$',
+ jba jsonb[] PATH '$',
+ NESTED PATH '$[1]' AS p1 COLUMNS (
+ a1 int,
+ NESTED PATH '$[*]' AS "p1 1" COLUMNS (
+ a11 text
+ ),
+ b1 text
+ ),
+ NESTED PATH '$[2]' AS p2 COLUMNS (
+ NESTED PATH '$[*]' AS "p2:1" COLUMNS (
+ a21 text
+ ),
+ NESTED PATH '$[*]' AS p22 COLUMNS (
+ a22 text
+ )
+ )
+ )
+ );
+\sv jsonb_table_view
+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 '$', 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)))))
+(3 rows)
+
+DROP VIEW jsonb_table_view;
+DROP DOMAIN jsonb_test_domain;
+-- 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: cannot use more than one FOR ORDINALITY column
+LINE 1: ..._TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR OR...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, a int PATH '$' ERROR ON EMPTY)) jt;
+ id | a
+----+---
+ 1 | 1
+(1 row)
+
+-- JSON_TABLE: ON EMPTY/ON ERROR behavior
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js),
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt;
+ js | a
+-------+---
+ 1 | 1
+ "err" |
+(2 rows)
+
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js)
+ LEFT OUTER JOIN
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt
+ ON true;
+ERROR: invalid input syntax for type integer: "err"
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js)
+ LEFT OUTER JOIN
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
+ ON true;
+ERROR: invalid input syntax for type integer: "err"
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt;
+ERROR: no SQL/JSON item
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt;
+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
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;
+ a
+---
+ 2
+(1 row)
+
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;
+ a
+---
+ 2
+(1 row)
+
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt;
+ a
+---
+ 1
+(1 row)
+
+-- JSON_TABLE: EXISTS PATH types
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a'));
+ a
+---
+ 0
+(1 row)
+
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a'));
+ERROR: cannot cast type boolean to smallint
+LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a'));
+ERROR: cannot cast type boolean to bigint
+LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a'));
+ERROR: cannot cast type boolean to real
+LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a'));
+ a
+-----
+ fal
+(1 row)
+
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a'));
+ERROR: cannot cast type boolean to json
+LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a'));
+ERROR: cannot cast type boolean to jsonb
+LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX...
+ ^
+-- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns don't work without an
+-- explicit FORMAT clause
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' KEEP QUOTES ON SCALAR STRING));
+ERROR: cannot use OMIT/KEEP QUOTES clause for scalar columns without also specifying FORMAT clause
+LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' KEEP QUOTES ON SCALAR STRING));
+ item
+---------
+ "world"
+(1 row)
+
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' WITH WRAPPER));
+ERROR: cannot use WITH WRAPPER clause for scalar columns without also specifying FORMAT clause
+LINE 1: ...T * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text ...
+ ^
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' WITH WRAPPER));
+ item
+-----------
+ ["world"]
+(1 row)
+
+-- JSON_TABLE: nested paths and plans
+-- Should fail (JSON_TABLE columns must contain explicit AS path
+-- specifications if explicit PLAN clause is used)
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' -- AS <path name> required here
+ COLUMNS (
+ foo int PATH '$'
+ )
+ PLAN DEFAULT (UNION)
+) jt;
+ERROR: invalid JSON_TABLE expression
+LINE 2: jsonb '[]', '$' -- AS <path name> required here
+ ^
+DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS path1
+ COLUMNS (
+ NESTED PATH '$' COLUMNS ( -- AS <path name> required here
+ foo int PATH '$'
+ )
+ )
+ PLAN DEFAULT (UNION)
+) jt;
+ERROR: invalid JSON_TABLE expression
+LINE 4: NESTED PATH '$' COLUMNS ( -- AS <path name> required here
+ ^
+DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used
+-- Should fail (column names must be distinct)
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS a
+ COLUMNS (
+ a int
+ )
+) jt;
+ERROR: duplicate JSON_TABLE column name: a
+HINT: JSON_TABLE column names must be distinct from one another.
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS a
+ COLUMNS (
+ b int,
+ NESTED PATH '$' AS a
+ COLUMNS (
+ c int
+ )
+ )
+) jt;
+ERROR: duplicate JSON_TABLE column name: a
+HINT: JSON_TABLE column names must be distinct from one another.
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$'
+ COLUMNS (
+ b int,
+ NESTED PATH '$' AS b
+ COLUMNS (
+ c int
+ )
+ )
+) jt;
+ERROR: duplicate JSON_TABLE column name: b
+HINT: JSON_TABLE column names must be distinct from one another.
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$'
+ COLUMNS (
+ NESTED PATH '$' AS a
+ COLUMNS (
+ b int
+ ),
+ NESTED PATH '$'
+ COLUMNS (
+ NESTED PATH '$' AS a
+ COLUMNS (
+ c int
+ )
+ )
+ )
+) jt;
+ERROR: duplicate JSON_TABLE column name: a
+HINT: JSON_TABLE column names must be distinct from one another.
+-- JSON_TABLE: plan validation
+SELECT * FROM JSON_TABLE(
+ jsonb 'null', '$[*]' AS p0
+ COLUMNS (
+ NESTED '$' 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 plan
+LINE 4: NESTED PATH '$' AS p1 COLUMNS (
+ ^
+DETAIL: Plan node for nested path p1 was not found in plan.
+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 plan
+LINE 4: NESTED PATH '$' AS p1 COLUMNS (
+ ^
+DETAIL: Plan node for nested path p1 was not found in plan.
+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
+LINE 12: PLAN (p0 UNION p1 UNION p11)
+ ^
+DETAIL: Expected INNER or OUTER JSON_TABLE plan node.
+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 plan
+LINE 8: NESTED PATH '$' AS p2 COLUMNS (
+ ^
+DETAIL: Plan node for nested path p2 was not found in plan.
+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 plan
+LINE 5: NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+ ^
+DETAIL: Plan node for nested path p11 was not found in plan.
+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
+LINE 12: PLAN (p0 OUTER ((p1 UNION p11) CROSS p2))
+ ^
+DETAIL: Plan node 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 plan
+LINE 6: NESTED PATH '$' AS p12 COLUMNS ( bar int )
+ ^
+DETAIL: Plan node for nested path p12 was not found in plan.
+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 plan
+LINE 9: NESTED PATH '$' AS p21 COLUMNS ( baz int )
+ ^
+DETAIL: Plan node for nested path p21 was not found in plan.
+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)
+
+SELECT * FROM JSON_TABLE(
+ jsonb 'null', 'strict $[*]' -- without root path name
+ 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 expression
+LINE 2: jsonb 'null', 'strict $[*]' -- without root path name
+ ^
+DETAIL: JSON_TABLE columns must contain explicit AS pathname specification if explicit PLAN clause is used
+-- JSON_TABLE: plan execution
+CREATE TEMP TABLE jsonb_table_test (js jsonb);
+INSERT INTO jsonb_table_test
+VALUES (
+ '[
+ {"a": 1, "b": [], "c": []},
+ {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]},
+ {"a": 3, "b": [1, 2], "c": []},
+ {"x": "4", "b": [1, 2], "c": 123}
+ ]'
+);
+-- unspecified 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 '$' )
+ )
+ ) 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)
+
+-- 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 | -1 | | | | |
+(20 rows)
+
+-- Should succeed (JSON arguments are passed to root and nested paths)
+SELECT *
+FROM
+ generate_series(1, 4) x,
+ generate_series(1, 3) y,
+ JSON_TABLE(jsonb
+ '[[1,2,3],[2,3,4,5],[3,4,5,6]]',
+ 'strict $[*] ? (@[*] < $x)'
+ PASSING x AS x, y AS y
+ COLUMNS (
+ y text FORMAT JSON PATH '$',
+ NESTED PATH 'strict $[*] ? (@ >= $y)'
+ COLUMNS (
+ z int PATH '$'
+ )
+ )
+ ) jt;
+ x | y | y | z
+---+---+--------------+---
+ 2 | 1 | [1, 2, 3] | 1
+ 2 | 1 | [1, 2, 3] | 2
+ 2 | 1 | [1, 2, 3] | 3
+ 3 | 1 | [1, 2, 3] | 1
+ 3 | 1 | [1, 2, 3] | 2
+ 3 | 1 | [1, 2, 3] | 3
+ 3 | 1 | [2, 3, 4, 5] | 2
+ 3 | 1 | [2, 3, 4, 5] | 3
+ 3 | 1 | [2, 3, 4, 5] | 4
+ 3 | 1 | [2, 3, 4, 5] | 5
+ 4 | 1 | [1, 2, 3] | 1
+ 4 | 1 | [1, 2, 3] | 2
+ 4 | 1 | [1, 2, 3] | 3
+ 4 | 1 | [2, 3, 4, 5] | 2
+ 4 | 1 | [2, 3, 4, 5] | 3
+ 4 | 1 | [2, 3, 4, 5] | 4
+ 4 | 1 | [2, 3, 4, 5] | 5
+ 4 | 1 | [3, 4, 5, 6] | 3
+ 4 | 1 | [3, 4, 5, 6] | 4
+ 4 | 1 | [3, 4, 5, 6] | 5
+ 4 | 1 | [3, 4, 5, 6] | 6
+ 2 | 2 | [1, 2, 3] | 2
+ 2 | 2 | [1, 2, 3] | 3
+ 3 | 2 | [1, 2, 3] | 2
+ 3 | 2 | [1, 2, 3] | 3
+ 3 | 2 | [2, 3, 4, 5] | 2
+ 3 | 2 | [2, 3, 4, 5] | 3
+ 3 | 2 | [2, 3, 4, 5] | 4
+ 3 | 2 | [2, 3, 4, 5] | 5
+ 4 | 2 | [1, 2, 3] | 2
+ 4 | 2 | [1, 2, 3] | 3
+ 4 | 2 | [2, 3, 4, 5] | 2
+ 4 | 2 | [2, 3, 4, 5] | 3
+ 4 | 2 | [2, 3, 4, 5] | 4
+ 4 | 2 | [2, 3, 4, 5] | 5
+ 4 | 2 | [3, 4, 5, 6] | 3
+ 4 | 2 | [3, 4, 5, 6] | 4
+ 4 | 2 | [3, 4, 5, 6] | 5
+ 4 | 2 | [3, 4, 5, 6] | 6
+ 2 | 3 | [1, 2, 3] | 3
+ 3 | 3 | [1, 2, 3] | 3
+ 3 | 3 | [2, 3, 4, 5] | 3
+ 3 | 3 | [2, 3, 4, 5] | 4
+ 3 | 3 | [2, 3, 4, 5] | 5
+ 4 | 3 | [1, 2, 3] | 3
+ 4 | 3 | [2, 3, 4, 5] | 3
+ 4 | 3 | [2, 3, 4, 5] | 4
+ 4 | 3 | [2, 3, 4, 5] | 5
+ 4 | 3 | [3, 4, 5, 6] | 3
+ 4 | 3 | [3, 4, 5, 6] | 4
+ 4 | 3 | [3, 4, 5, 6] | 5
+ 4 | 3 | [3, 4, 5, 6] | 6
+(52 rows)
+
+-- Should fail (JSON arguments are not passed to column paths)
+SELECT *
+FROM JSON_TABLE(
+ jsonb '[1,2,3]',
+ '$[*] ? (@ < $x)'
+ PASSING 10 AS x
+ COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)')
+ ) jt;
+ERROR: could not find jsonpath variable "x"
+-- Extension: non-constant JSON path
+SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a');
+ json_value
+------------
+ 123
+(1 row)
+
+SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY);
+ json_value
+------------
+ foo
+(1 row)
+
+SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a');
+ json_query
+------------
+ 123
+(1 row)
+
+SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER);
+ json_query
+------------
+ [123]
+(1 row)
+
+-- Should fail (invalid path)
+SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error');
+ERROR: syntax error at or near " " of jsonpath input
+-- Should fail (not supported)
+SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int));
+ERROR: only string constants supported in JSON_TABLE path specification
+LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '...
+ ^
diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql
index 4f30fa46b9..df4a430d88 100644
--- a/src/test/regress/sql/json_sqljson.sql
+++ b/src/test/regress/sql/json_sqljson.sql
@@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$');
-- JSON_QUERY
SELECT JSON_QUERY(NULL FORMAT JSON, '$');
+
+-- JSON_TABLE
+
+SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text));
diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql
index 77bfb6d61b..ea9b4ff8b6 100644
--- a/src/test/regress/sql/jsonb_sqljson.sql
+++ b/src/test/regress/sql/jsonb_sqljson.sql
@@ -335,3 +335,677 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime()
CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x));
CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x));
DROP TABLE test_jsonb_mutability;
+
+-- JSON_TABLE
+
+-- Should fail (JSON_TABLE can be used only in FROM clause)
+SELECT JSON_TABLE('[]', '$');
+
+-- Should fail (no columns)
+SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ());
+
+SELECT * FROM JSON_TABLE (NULL::jsonb, '$' COLUMNS (v1 timestamp)) AS f (v1, v2);
+
+-- NULL => empty table
+SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar;
+
+--
+SELECT * FROM JSON_TABLE(jsonb '123', '$'
+ COLUMNS (item int PATH '$', foo int)) bar;
+
+-- JSON_TABLE: basic functionality
+CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo');
+CREATE TEMP TABLE json_table_test (js) AS
+ (VALUES
+ ('1'),
+ ('[]'),
+ ('{}'),
+ ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]')
+ );
+
+-- Regular "unformatted" columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ "int" int PATH '$',
+ "text" text PATH '$',
+ "char(4)" char(4) PATH '$',
+ "bool" bool PATH '$',
+ "numeric" numeric PATH '$',
+ "domain" jsonb_test_domain PATH '$',
+ js json PATH '$',
+ jb jsonb PATH '$'
+ )
+ ) jt
+ ON true;
+
+-- "formatted" columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ jst text FORMAT JSON PATH '$',
+ jsc char(4) FORMAT JSON PATH '$',
+ jsv varchar(4) FORMAT JSON PATH '$',
+ jsb jsonb FORMAT JSON PATH '$',
+ jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES
+ )
+ ) jt
+ ON true;
+
+-- EXISTS columns
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ exists1 bool EXISTS PATH '$.aaa',
+ exists2 int EXISTS PATH '$.aaa',
+ exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR,
+ exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR
+ )
+ ) jt
+ ON true;
+
+-- Other miscellanous checks
+SELECT *
+FROM json_table_test vals
+ LEFT OUTER JOIN
+ JSON_TABLE(
+ vals.js::jsonb, 'lax $[*]'
+ COLUMNS (
+ id FOR ORDINALITY,
+ aaa int, -- "aaa" has implicit path '$."aaa"'
+ aaa1 int PATH '$.aaa',
+ js2 json PATH '$',
+ jsb2w jsonb PATH '$' WITH WRAPPER,
+ jsb2q jsonb PATH '$' OMIT QUOTES,
+ ia int[] PATH '$',
+ ta text[] PATH '$',
+ jba jsonb[] PATH '$'
+ )
+ ) jt
+ ON true;
+
+-- JSON_TABLE: Test backward parsing
+
+CREATE VIEW jsonb_table_view AS
+SELECT * FROM
+ JSON_TABLE(
+ jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c"
+ COLUMNS (
+ id FOR ORDINALITY,
+ "int" int PATH '$',
+ "text" text PATH '$',
+ "char(4)" char(4) PATH '$',
+ "bool" bool PATH '$',
+ "numeric" numeric PATH '$',
+ "domain" jsonb_test_domain PATH '$',
+ js json PATH '$',
+ jb jsonb PATH '$',
+ jst text FORMAT JSON PATH '$',
+ jsc char(4) FORMAT JSON PATH '$',
+ jsv varchar(4) FORMAT JSON PATH '$',
+ jsb jsonb FORMAT JSON PATH '$',
+ jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES,
+ aaa int, -- implicit path '$."aaa"',
+ aaa1 int PATH '$.aaa',
+ exists1 bool EXISTS PATH '$.aaa',
+ exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR,
+ exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR,
+
+ js2 json PATH '$',
+ jsb2w jsonb PATH '$' WITH WRAPPER,
+ jsb2q jsonb PATH '$' OMIT QUOTES,
+ ia int[] PATH '$',
+ ta text[] PATH '$',
+ jba jsonb[] PATH '$',
+
+ NESTED PATH '$[1]' AS p1 COLUMNS (
+ a1 int,
+ NESTED PATH '$[*]' AS "p1 1" COLUMNS (
+ a11 text
+ ),
+ b1 text
+ ),
+ NESTED PATH '$[2]' AS p2 COLUMNS (
+ NESTED PATH '$[*]' AS "p2:1" COLUMNS (
+ a21 text
+ ),
+ NESTED PATH '$[*]' AS p22 COLUMNS (
+ a22 text
+ )
+ )
+ )
+ );
+
+\sv jsonb_table_view
+
+EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view;
+
+DROP VIEW jsonb_table_view;
+DROP DOMAIN jsonb_test_domain;
+
+-- 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;
+
+-- JSON_TABLE: ON EMPTY/ON ERROR behavior
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js),
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt;
+
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js)
+ LEFT OUTER JOIN
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt
+ ON true;
+
+SELECT *
+FROM
+ (VALUES ('1'), ('"err"')) vals(js)
+ LEFT OUTER JOIN
+ JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt
+ ON true;
+
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt;
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt;
+SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) 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;
+
+-- JSON_TABLE: EXISTS PATH types
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a'));
+SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a'));
+
+-- JSON_TABLE: WRAPPER/QUOTES clauses on scalar columns don't work without an
+-- explicit FORMAT clause
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' KEEP QUOTES ON SCALAR STRING));
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' KEEP QUOTES ON SCALAR STRING));
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text PATH '$' WITH WRAPPER));
+SELECT * FROM JSON_TABLE(jsonb '"world"', '$' COLUMNS (item text FORMAT JSON PATH '$' WITH WRAPPER));
+
+-- JSON_TABLE: nested paths and plans
+
+-- Should fail (JSON_TABLE columns must contain explicit AS path
+-- specifications if explicit PLAN clause is used)
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' -- AS <path name> required here
+ COLUMNS (
+ foo int PATH '$'
+ )
+ PLAN DEFAULT (UNION)
+) jt;
+
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS path1
+ COLUMNS (
+ NESTED PATH '$' COLUMNS ( -- AS <path name> required here
+ foo int PATH '$'
+ )
+ )
+ PLAN DEFAULT (UNION)
+) jt;
+
+-- Should fail (column names must be distinct)
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS a
+ COLUMNS (
+ a int
+ )
+) jt;
+
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$' AS a
+ COLUMNS (
+ b int,
+ NESTED PATH '$' AS a
+ COLUMNS (
+ c int
+ )
+ )
+) jt;
+
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$'
+ COLUMNS (
+ b int,
+ NESTED PATH '$' AS b
+ COLUMNS (
+ c int
+ )
+ )
+) jt;
+
+SELECT * FROM JSON_TABLE(
+ jsonb '[]', '$'
+ COLUMNS (
+ NESTED PATH '$' AS a
+ COLUMNS (
+ b int
+ ),
+ NESTED PATH '$'
+ COLUMNS (
+ NESTED PATH '$' AS a
+ COLUMNS (
+ c int
+ )
+ )
+ )
+) jt;
+
+-- JSON_TABLE: plan validation
+
+SELECT * FROM JSON_TABLE(
+ jsonb 'null', '$[*]' AS p0
+ COLUMNS (
+ NESTED '$' 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;
+
+SELECT * FROM JSON_TABLE(
+ jsonb 'null', 'strict $[*]' -- without root path name
+ 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
+
+CREATE TEMP TABLE jsonb_table_test (js jsonb);
+
+INSERT INTO jsonb_table_test
+VALUES (
+ '[
+ {"a": 1, "b": [], "c": []},
+ {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]},
+ {"a": 3, "b": [1, 2], "c": []},
+ {"x": "4", "b": [1, 2], "c": 123}
+ ]'
+);
+
+-- unspecified 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 '$' )
+ )
+ ) 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;
+
+-- Should succeed (JSON arguments are passed to root and nested paths)
+SELECT *
+FROM
+ generate_series(1, 4) x,
+ generate_series(1, 3) y,
+ JSON_TABLE(jsonb
+ '[[1,2,3],[2,3,4,5],[3,4,5,6]]',
+ 'strict $[*] ? (@[*] < $x)'
+ PASSING x AS x, y AS y
+ COLUMNS (
+ y text FORMAT JSON PATH '$',
+ NESTED PATH 'strict $[*] ? (@ >= $y)'
+ COLUMNS (
+ z int PATH '$'
+ )
+ )
+ ) jt;
+
+-- Should fail (JSON arguments are not passed to column paths)
+SELECT *
+FROM JSON_TABLE(
+ jsonb '[1,2,3]',
+ '$[*] ? (@ < $x)'
+ PASSING 10 AS x
+ COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)')
+ ) jt;
+
+-- Extension: non-constant JSON path
+SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a');
+SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a');
+SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY);
+SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a');
+SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER);
+-- Should fail (invalid path)
+SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error');
+-- Should fail (not supported)
+SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int));
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9ee58ec2b0..a09df0e641 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1315,6 +1315,7 @@ JsonPathMutableContext
JsonPathParseItem
JsonPathParseResult
JsonPathPredicateCallback
+JsonPathSpec
JsonPathString
JsonPathVarCallback
JsonPathVariable
@@ -1324,6 +1325,17 @@ JsonReturning
JsonScalarExpr
JsonSemAction
JsonSerializeExpr
+JsonTable
+JsonTableColumn
+JsonTableColumnType
+JsonTableContext
+JsonTableJoinState
+JsonTableParent
+JsonTablePlan
+JsonTablePlanJoinType
+JsonTablePlanType
+JsonTableScanState
+JsonTableSibling
JsonTokenType
JsonTransformStringValuesAction
JsonTypeCategory
@@ -2787,6 +2799,7 @@ TableFunc
TableFuncRoutine
TableFuncScan
TableFuncScanState
+TableFuncType
TableInfo
TableLikeClause
TableSampleClause
--
2.35.3
[application/octet-stream] v28-0003-SQL-JSON-query-functions.patch (196.7K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/6-v28-0003-SQL-JSON-query-functions.patch)
download | inline diff:
From a99f2c9fe40a33d08968d01fe035f27db25b531e Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Tue, 5 Dec 2023 14:33:25 +0900
Subject: [PATCH v28 3/6] SQL/JSON query functions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This introduces the SQL/JSON functions for querying JSON data using
jsonpath expressions. The functions are:
JSON_EXISTS()
JSON_QUERY()
JSON_VALUE()
JSON_EXISTS() tests if the jsonpath expression applied to the jsonb
value yields any values.
JSON_VALUE() must return a single value, and an error occurs if it
tries to return multiple values.
JSON_QUERY() must return a json object or array, and there are
various WRAPPER options for handling scalar or multi-value results.
Both these functions have options for handling EMPTY and ERROR
conditions.
All of these functions only operate on jsonb. The workaround for now
is to cast the argument to jsonb.
Author: Nikita Glukhov <[email protected]>
Author: Teodor Sigaev <[email protected]>
Author: Oleg Bartunov <[email protected]>
Author: Alexander Korotkov <[email protected]>
Author: Andrew Dunstan <[email protected]>
Author: Amit Langote <[email protected]>
Author: Peter Eisentraut <[email protected]>
Author: jian he <[email protected]>
Reviewers have included (in no particular order) Andres Freund, Alexander
Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu,
Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera,
jian he, Anton A. Melnikov, Nikita Malakhov, Peter Eisentraut
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/[email protected]
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
---
doc/src/sgml/func.sgml | 151 +++
src/backend/catalog/sql_features.txt | 12 +-
src/backend/executor/execExpr.c | 363 +++++++
src/backend/executor/execExprInterp.c | 367 ++++++-
src/backend/jit/llvm/llvmjit.c | 2 +
src/backend/jit/llvm/llvmjit_expr.c | 140 +++
src/backend/jit/llvm/llvmjit_types.c | 4 +
src/backend/nodes/makefuncs.c | 18 +
src/backend/nodes/nodeFuncs.c | 238 ++++-
src/backend/optimizer/path/costsize.c | 3 +-
src/backend/optimizer/util/clauses.c | 19 +
src/backend/parser/gram.y | 178 +++-
src/backend/parser/parse_expr.c | 649 +++++++++++-
src/backend/parser/parse_target.c | 15 +
src/backend/utils/adt/formatting.c | 44 +
src/backend/utils/adt/jsonb.c | 31 +
src/backend/utils/adt/jsonfuncs.c | 52 +-
src/backend/utils/adt/jsonpath.c | 255 +++++
src/backend/utils/adt/jsonpath_exec.c | 391 ++++++-
src/backend/utils/adt/ruleutils.c | 136 +++
src/include/executor/execExpr.h | 133 +++
src/include/fmgr.h | 1 +
src/include/jit/llvmjit.h | 1 +
src/include/nodes/makefuncs.h | 2 +
src/include/nodes/parsenodes.h | 47 +
src/include/nodes/primnodes.h | 130 +++
src/include/parser/kwlist.h | 11 +
src/include/utils/formatting.h | 1 +
src/include/utils/jsonb.h | 1 +
src/include/utils/jsonfuncs.h | 5 +
src/include/utils/jsonpath.h | 27 +
src/interfaces/ecpg/preproc/ecpg.trailer | 28 +
src/test/regress/expected/json_sqljson.out | 18 +
src/test/regress/expected/jsonb_sqljson.out | 1032 +++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/json_sqljson.sql | 11 +
src/test/regress/sql/jsonb_sqljson.sql | 337 ++++++
src/tools/pgindent/typedefs.list | 19 +
38 files changed, 4798 insertions(+), 76 deletions(-)
create mode 100644 src/test/regress/expected/json_sqljson.out
create mode 100644 src/test/regress/expected/jsonb_sqljson.out
create mode 100644 src/test/regress/sql/json_sqljson.sql
create mode 100644 src/test/regress/sql/jsonb_sqljson.sql
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 20da3ed033..7962a8a1a4 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -18156,6 +18156,157 @@ $.* ? (@ like_regex "^\\d+$")
</programlisting>
</para>
</sect3>
+
+ <sect3 id="sqljson-query-functions">
+ <title>SQL/JSON Query Functions</title>
+ <para>
+ <xref linkend="functions-sqljson-querying"/> details the SQL/JSON
+ functions that can be used to query JSON data.
+ </para>
+
+ <note>
+ <para>
+ SQL/JSON path expression can currently only accept values of the
+ <type>jsonb</type> type, so it might be necessary to cast the
+ <replaceable>context_item</replaceable> argument of these functions to
+ <type>jsonb</type>.
+ </para>
+ </note>
+
+ <table id="functions-sqljson-querying">
+ <title>SQL/JSON Query Functions</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ Function signature
+ </para>
+ <para>
+ Description
+ </para>
+ <para>
+ Example(s)
+ </para></entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm><primary>json_exists</primary></indexterm>
+ <function>json_exists</function> (
+ <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional>
+ <optional> { <literal>TRUE</literal> | <literal>FALSE</literal> |<literal> UNKNOWN</literal> | <literal>ERROR</literal> } <literal>ON ERROR</literal> </optional>)
+ </para>
+ <para>
+ Returns true if the SQL/JSON <replaceable>path_expression</replaceable>
+ applied to the <replaceable>context_item</replaceable> using the
+ <replaceable>value</replaceable>s yields any items.
+ The <literal>ON ERROR</literal> clause specifies what is returned if
+ an error occurs; the default is to return <literal>FALSE</literal>.
+ Note that if the <replaceable>path_expression</replaceable>
+ is <literal>strict</literal>, an error is generated if it yields no items.
+ </para>
+ <para>
+ <literal>json_exists(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > 2)')</literal>
+ <returnvalue>t</returnvalue>
+ </para>
+ <para>
+ <literal>json_exists(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR)</literal>
+ <returnvalue>f</returnvalue>
+ </para>
+ <para>
+ <literal>json_exists(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR)</literal>
+ <returnvalue>ERROR: jsonpath array subscript is out of bounds</returnvalue>
+ </para></entry>
+ </row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm><primary>json_query</primary></indexterm>
+ <function>json_query</function> (
+ <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional>
+ <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> <optional> <literal>FORMAT JSON</literal> <optional> <literal>ENCODING UTF8</literal> </optional> </optional> </optional>
+ <optional> { <literal>WITHOUT</literal> | <literal>WITH</literal> { <literal>CONDITIONAL</literal> | <optional><literal>UNCONDITIONAL</literal></optional> } } <optional> <literal>ARRAY</literal> </optional> <literal>WRAPPER</literal> </optional>
+ <optional> { <literal>KEEP</literal> | <literal>OMIT</literal> } <literal>QUOTES</literal> <optional> <literal>ON SCALAR STRING</literal> </optional> </optional>
+ <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional>
+ <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>EMPTY</literal> { <optional> <literal>ARRAY</literal> </optional> | <literal>OBJECT</literal> } | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>)
+ </para>
+ <para>
+ Returns the result of applying the
+ <replaceable>path_expression</replaceable> to the
+ <replaceable>context_item</replaceable> using the
+ <replaceable>value</replaceable>s.
+ This function must return a JSON string, so if the path expression
+ returns multiple SQL/JSON items, you must wrap the result using the
+ <literal>WITH WRAPPER</literal> clause. If the wrapper is
+ <literal>UNCONDITIONAL</literal>, an array wrapper will always
+ be applied, even if the returned value is already a single JSON object
+ or an array, but if it is <literal>CONDITIONAL</literal>, it will not be
+ applied to a single array or object. <literal>UNCONDITIONAL</literal>
+ is the default. If the result is a scalar string, by default the value
+ returned will have surrounding quotes making it a valid JSON value,
+ which can be made explicit by specifying <literal>KEEP QUOTES</literal>.
+ Conversely, quotes can be omitted by specifying <literal>OMIT QUOTES</literal>.
+ The returned <replaceable>data_type</replaceable> has the same semantics
+ as for constructor functions like <function>json_objectagg</function>;
+ the default returned type is <type>jsonb</type>.
+ The <literal>ON EMPTY</literal> clause specifies the behavior if the
+ <replaceable>path_expression</replaceable> yields no value at all; the
+ default when <literal>ON EMPTY</literal> is not specified is to return a
+ null value. The <literal>ON ERROR</literal> clause specifies the behavior
+ if an error occurs as a result of <type>jsonpath</type> evaluation
+ (including cast to the output type) or during the execution of
+ <literal>ON EMPTY</literal> behavior (that was caused by empty result of
+ <type>jsonpath</type> evaluation); the default when
+ <literal>ON ERROR</literal> is not specified is to return a null value.
+ </para>
+ <para>
+ <literal>json_query(jsonb '[1,[2,3],null]', 'lax $[*][1]' WITH CONDITIONAL WRAPPER)</literal>
+ <returnvalue>[3]</returnvalue>
+ </para></entry>
+ </row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm><primary>json_value</primary></indexterm>
+ <function>json_value</function> (
+ <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable>
+ <optional> <literal>PASSING</literal> { <replaceable>value</replaceable> <literal>AS</literal> <replaceable>varname</replaceable> } <optional>, ...</optional></optional>
+ <optional> <literal>RETURNING</literal> <replaceable>data_type</replaceable> </optional>
+ <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON EMPTY</literal> </optional>
+ <optional> { <literal>ERROR</literal> | <literal>NULL</literal> | <literal>DEFAULT</literal> <replaceable>expression</replaceable> } <literal>ON ERROR</literal> </optional>)
+ </para>
+ <para>
+ Returns the result of applying the
+ <replaceable>path_expression</replaceable> to the
+ <replaceable>context_item</replaceable> using the
+ <literal>PASSING</literal> <replaceable>value</replaceable>s. The
+ extracted value must be a single <acronym>SQL/JSON</acronym> scalar
+ item. For results that are objects or arrays, use the
+ <function>json_query</function> function instead.
+ The returned <replaceable>data_type</replaceable> has the same semantics
+ as for constructor functions like <function>json_objectagg</function>.
+ The default returned type is <type>text</type>.
+ The <literal>ON ERROR</literal> and <literal>ON EMPTY</literal>
+ clauses have similar semantics as mentioned in the description of
+ <function>json_query</function>.
+ </para>
+ <para>
+ <literal>json_value(jsonb '"123.45"', '$' RETURNING float)</literal>
+ <returnvalue>123.45</returnvalue>
+ </para>
+ <para>
+ <literal>json_value(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI YYYY-MM-DD")' RETURNING date)</literal>
+ <returnvalue>2015-02-01</returnvalue>
+ </para>
+ <para>
+ <literal>json_value(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR)</literal>
+ <returnvalue>9</returnvalue>
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect3>
</sect2>
</sect1>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 80c40eaf57..7598bd8f22 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -548,15 +548,15 @@ T811 Basic SQL/JSON constructor functions YES
T812 SQL/JSON: JSON_OBJECTAGG YES
T813 SQL/JSON: JSON_ARRAYAGG with ORDER BY YES
T814 Colon in JSON_OBJECT or JSON_OBJECTAGG YES
-T821 Basic SQL/JSON query operators NO
+T821 Basic SQL/JSON query operators YES
T822 SQL/JSON: IS JSON WITH UNIQUE KEYS predicate YES
-T823 SQL/JSON: PASSING clause NO
+T823 SQL/JSON: PASSING clause YES
T824 JSON_TABLE: specific PLAN clause NO
-T825 SQL/JSON: ON EMPTY and ON ERROR clauses NO
-T826 General value expression in ON ERROR or ON EMPTY clauses NO
+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 NO
-T828 JSON_QUERY NO
-T829 JSON_QUERY: array wrapper options NO
+T828 JSON_QUERY YES
+T829 JSON_QUERY: array wrapper options YES
T830 Enforcing unique keys in SQL/JSON constructor functions YES
T831 SQL/JSON path language: strict mode YES
T832 SQL/JSON path language: item method YES
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 34bd2102b5..b367c64a00 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -49,6 +49,7 @@
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/jsonfuncs.h"
+#include "utils/jsonpath.h"
#include "utils/lsyscache.h"
#include "utils/typcache.h"
@@ -88,6 +89,12 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate,
FunctionCallInfo fcinfo, AggStatePerTrans pertrans,
int transno, int setno, int setoff, bool ishash,
bool nullcheck);
+static void ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state,
+ Datum *resv, bool *resnull,
+ ExprEvalStep *scratch);
+static int ExecInitJsonExprCoercion(ExprState *state, Node *coercion,
+ ErrorSaveContext *escontext,
+ Datum *resv, bool *resnull);
/*
@@ -2416,6 +2423,36 @@ ExecInitExprRec(Expr *node, ExprState *state,
break;
}
+ case T_JsonExpr:
+ {
+ JsonExpr *jexpr = castNode(JsonExpr, node);
+
+ ExecInitJsonExpr(jexpr, state, resv, resnull, &scratch);
+ break;
+ }
+
+ case T_JsonCoercion:
+ {
+ JsonCoercion *coercion = castNode(JsonCoercion, node);
+ JsonCoercionState *jcstate = palloc0(sizeof(JsonCoercionState));
+ Oid typinput;
+ FmgrInfo *finfo;
+
+ getTypeInputInfo(coercion->targettype, &typinput,
+ &jcstate->input.typioparam);
+ finfo = palloc0(sizeof(FmgrInfo));
+ fmgr_info(typinput, finfo);
+ jcstate->input.finfo = finfo;
+
+ jcstate->coercion = coercion;
+ jcstate->escontext = state->escontext;
+
+ scratch.opcode = EEOP_JSONEXPR_COERCION;
+ scratch.d.jsonexpr_coercion.jcstate = jcstate;
+ ExprEvalPushStep(state, &scratch);
+ break;
+ }
+
case T_NullTest:
{
NullTest *ntest = (NullTest *) node;
@@ -4184,3 +4221,329 @@ ExecBuildParamSetEqual(TupleDesc desc,
return state;
}
+
+/*
+ * Push steps to evaluate a JsonExpr and its various subsidiary expressions.
+ */
+static void
+ExecInitJsonExpr(JsonExpr *jexpr, ExprState *state,
+ Datum *resv, bool *resnull,
+ ExprEvalStep *scratch)
+{
+ JsonExprState *jsestate = palloc0(sizeof(JsonExprState));
+ JsonExprPostEvalState *post_eval = &jsestate->post_eval;
+ ListCell *argexprlc;
+ ListCell *argnamelc;
+ List *jumps_if_skip = NIL;
+ List *jumps_to_coerce_finish = NIL;
+ List *jumps_to_end = NIL;
+ ListCell *lc;
+ ExprEvalStep *as;
+
+ jsestate->jsexpr = jexpr;
+
+ /*
+ * Evaluate formatted_expr storing the result into
+ * jsestate->formatted_expr.
+ */
+ ExecInitExprRec((Expr *) jexpr->formatted_expr, state,
+ &jsestate->formatted_expr.value,
+ &jsestate->formatted_expr.isnull);
+
+ /* Steps to jump to end if formatted_expr evaluates to NULL */
+ scratch->opcode = EEOP_JUMP_IF_NULL;
+ scratch->resnull = &jsestate->formatted_expr.isnull;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_if_skip = lappend_int(jumps_if_skip, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+
+ /*
+ * Evaluate pathspec expression storing the result into
+ * jsestate->pathspec.
+ */
+ ExecInitExprRec((Expr *) jexpr->path_spec, state,
+ &jsestate->pathspec.value,
+ &jsestate->pathspec.isnull);
+
+ /* Steps to JUMP to end if pathspec evaluates to NULL */
+ scratch->opcode = EEOP_JUMP_IF_NULL;
+ scratch->resnull = &jsestate->pathspec.isnull;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_if_skip = lappend_int(jumps_if_skip, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+
+ /* Steps to compute PASSING args. */
+ jsestate->args = NIL;
+ forboth(argexprlc, jexpr->passing_values,
+ argnamelc, jexpr->passing_names)
+ {
+ Expr *argexpr = (Expr *) lfirst(argexprlc);
+ String *argname = lfirst_node(String, argnamelc);
+ JsonPathVariable *var = palloc(sizeof(*var));
+
+ var->name = argname->sval;
+ var->typid = exprType((Node *) argexpr);
+ var->typmod = exprTypmod((Node *) argexpr);
+
+ ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull);
+
+ jsestate->args = lappend(jsestate->args, var);
+ }
+
+ /* Step for JsonPath* evaluation; see ExecEvalJsonExprPath(). */
+ scratch->opcode = EEOP_JSONEXPR_PATH;
+ scratch->resvalue = resv;
+ scratch->resnull = resnull;
+ scratch->d.jsonexpr.jsestate = jsestate;
+ ExprEvalPushStep(state, scratch);
+
+ /*
+ * Step to jump to end when there's neither an error when evaluating
+ * JsonPath* nor any need to coerce the result because it's already
+ * of the specified type.
+ */
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_end = lappend_int(jumps_to_end, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+
+ /*
+ * Steps to coerce the result value computed by EEOP_JSONEXPR_PATH.
+ * To handle coercion errors softly, use the following ErrorSaveContext
+ * when initializing the coercion expressions, including any JsonCoercion
+ * nodes.
+ */
+ jsestate->escontext.type = T_ErrorSaveContext;
+ if (jexpr->result_coercion || jexpr->omit_quotes)
+ {
+ jsestate->jump_eval_result_coercion =
+ ExecInitJsonExprCoercion(state, jexpr->result_coercion,
+ jexpr->on_error->btype != JSON_BEHAVIOR_ERROR ?
+ &jsestate->escontext : NULL,
+ resv, resnull);
+ }
+ else
+ jsestate->jump_eval_result_coercion = -1;
+
+ /* Steps for coercing JsonItemType values returned by JsonPathValue(). */
+ if (jexpr->item_coercions)
+ {
+ /*
+ * Jump to COERCION_FINISH to skip over the following steps if
+ * result_coercion is present.
+ */
+ if (jsestate->jump_eval_result_coercion >= 0)
+ {
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_coerce_finish = lappend_int(jumps_to_coerce_finish,
+ state->steps_len);
+ ExprEvalPushStep(state, scratch);
+ }
+
+ /*
+ * Here we create the steps for each JsonItemType type's coercion
+ * expression and also store a flag whether the expression is
+ * a JsonCoercion node. ExecPrepareJsonItemCoercion() called by
+ * ExecEvalJsonExprPath() will map a given JsonbValue returned by
+ * JsonPathValue() to its JsonItemType's expression's step address
+ * and the flag by indexing the following arrays with JsonItemType
+ * enum value.
+ */
+ jsestate->num_item_coercions = list_length(jexpr->item_coercions);
+ jsestate->eval_item_coercion_jumps = (int *)
+ palloc(jsestate->num_item_coercions * sizeof(int));
+ jsestate->item_coercion_via_expr = (bool *)
+ palloc0(jsestate->num_item_coercions * sizeof(bool));
+ foreach(lc, jexpr->item_coercions)
+ {
+ JsonItemCoercion *item_coercion = lfirst(lc);
+ Node *coercion = item_coercion->coercion;
+
+ jsestate->item_coercion_via_expr[item_coercion->item_type] =
+ (coercion != NULL && !IsA(coercion, JsonCoercion));
+ jsestate->eval_item_coercion_jumps[item_coercion->item_type] =
+ ExecInitJsonExprCoercion(state, coercion,
+ jexpr->on_error->btype != JSON_BEHAVIOR_ERROR ?
+ &jsestate->escontext : NULL,
+ resv, resnull);
+
+ /* Emit JUMP step to skip past other coercions' steps. */
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_coerce_finish = lappend_int(jumps_to_coerce_finish,
+ state->steps_len);
+ ExprEvalPushStep(state, scratch);
+ }
+ }
+
+ /*
+ * Add step to reset the ErrorSaveContext and set error flag if the
+ * coercion steps encountered an error but was not thrown because of the
+ * ON ERROR behavior.
+ */
+ if (jexpr->result_coercion || jexpr->item_coercions)
+ {
+ foreach(lc, jumps_to_coerce_finish)
+ {
+ as = &state->steps[lfirst_int(lc)];
+ as->d.jump.jumpdone = state->steps_len;
+ }
+
+ scratch->opcode = EEOP_JSONEXPR_COERCION_FINISH;
+ scratch->d.jsonexpr.jsestate = jsestate;
+ ExprEvalPushStep(state, scratch);
+ }
+
+ /*
+ * Step to handle ON ERROR behaviors. This handles both the errors
+ * that occur during EEOP_JSONEXPR_PATH evaluation and subsequent coercion
+ * evaluation.
+ */
+ jsestate->jump_error = -1;
+ if (jexpr->on_error &&
+ jexpr->on_error->btype != JSON_BEHAVIOR_ERROR)
+ {
+ jsestate->jump_error = state->steps_len;
+ scratch->opcode = EEOP_JUMP_IF_NOT_TRUE;
+
+ /*
+ * post_eval.error is set by ExecEvalJsonExprPath() and
+ * ExecEvalJsonCoercionFinish().
+ */
+ scratch->resvalue = &post_eval->error.value;
+ scratch->resnull = &post_eval->error.isnull;
+
+ scratch->d.jump.jumpdone = -1; /* set below */
+ ExprEvalPushStep(state, scratch);
+
+ /* Steps to evaluate the ON ERROR expression */
+ ExecInitExprRec((Expr *) jexpr->on_error->expr,
+ state, resv, resnull);
+
+ /* Steps to coerce the ON ERROR expression if needed */
+ if (jexpr->on_error->coercion)
+ ExecInitExprRec((Expr *) jexpr->on_error->coercion, state,
+ resv, resnull);
+
+ jumps_to_end = lappend_int(jumps_to_end, state->steps_len);
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1;
+ ExprEvalPushStep(state, scratch);
+ }
+
+ /* Step to handle ON EMPTY behaviors. */
+ if (jexpr->on_empty != NULL &&
+ jexpr->on_empty->btype != JSON_BEHAVIOR_ERROR)
+ {
+ /*
+ * Make the ON ERROR behavior JUMP to here after checking the error
+ * and if it's not present then make EEOP_JSONEXPR_PATH directly
+ * jump here.
+ */
+ if (jsestate->jump_error >= 0)
+ {
+ as = &state->steps[jsestate->jump_error];
+ as->d.jump.jumpdone = state->steps_len;
+ }
+ else
+ jsestate->jump_error = state->steps_len;
+
+ scratch->opcode = EEOP_JUMP_IF_NOT_TRUE;
+ scratch->resvalue = &post_eval->empty.value;
+ scratch->resnull = &post_eval->empty.isnull;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_end = lappend_int(jumps_to_end, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+
+ /* Steps to evaluate the ON EMPTY expression */
+ ExecInitExprRec((Expr *) jexpr->on_empty->expr,
+ state, resv, resnull);
+
+ /* Steps to coerce the ON EMPTY expression if needed */
+ if (jexpr->on_empty->coercion)
+ ExecInitExprRec((Expr *) jexpr->on_empty->coercion, state,
+ resv, resnull);
+
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_end = lappend_int(jumps_to_end, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+ }
+ /* Make EEOP_JSONEXPR_PATH jump to end if no ON EMPTY clause present. */
+ else if (jsestate->jump_error >= 0)
+ jumps_to_end = lappend_int(jumps_to_end, jsestate->jump_error);
+
+ /*
+ * If neither ON ERROR nor ON EMPTY jumps present, then add one to go
+ * to end.
+ */
+ if (jsestate->jump_error < 0)
+ {
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = -1; /* set below */
+ jumps_to_end = lappend_int(jumps_to_end, state->steps_len);
+ ExprEvalPushStep(state, scratch);
+ }
+
+ /* Return NULL when either formatted_expr or pathspec is NULL. */
+ foreach(lc, jumps_if_skip)
+ {
+ as = &state->steps[lfirst_int(lc)];
+ as->d.jump.jumpdone = state->steps_len;
+ }
+ scratch->opcode = EEOP_CONST;
+ scratch->resvalue = resv;
+ scratch->resnull = resnull;
+ scratch->d.constval.value = (Datum) 0;
+ scratch->d.constval.isnull = true;
+ ExprEvalPushStep(state, scratch);
+
+ /* Jump to coerce the NULL using result_coercion is present. */
+ if (jsestate->jump_eval_result_coercion >= 0)
+ {
+ scratch->opcode = EEOP_JUMP;
+ scratch->d.jump.jumpdone = jsestate->jump_eval_result_coercion;
+ ExprEvalPushStep(state, scratch);
+ }
+
+ foreach(lc, jumps_to_end)
+ {
+ as = &state->steps[lfirst_int(lc)];
+ as->d.jump.jumpdone = state->steps_len;
+ }
+}
+
+/* Initialize one JsonCoercion for execution. */
+static int
+ExecInitJsonExprCoercion(ExprState *state, Node *coercion,
+ ErrorSaveContext *escontext,
+ Datum *resv, bool *resnull)
+{
+ int jump_eval_coercion;
+ Datum *save_innermost_caseval;
+ bool *save_innermost_casenull;
+ ErrorSaveContext *save_escontext;
+
+ if (coercion == NULL)
+ return -1;
+
+ jump_eval_coercion = state->steps_len;
+
+ /* Push step(s) to compute cstate->coercion. */
+ save_innermost_caseval = state->innermost_caseval;
+ save_innermost_casenull = state->innermost_casenull;
+ save_escontext = state->escontext;
+
+ state->innermost_caseval = resv;
+ state->innermost_casenull = resnull;
+ state->escontext = escontext;
+
+ ExecInitExprRec((Expr *) coercion, state, resv, resnull);
+
+ state->innermost_caseval = save_innermost_caseval;
+ state->innermost_casenull = save_innermost_casenull;
+ state->escontext = save_escontext;
+
+ return jump_eval_coercion;
+}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index d5db96444c..697b9a84a9 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -73,8 +73,8 @@
#include "utils/datum.h"
#include "utils/expandedrecord.h"
#include "utils/json.h"
-#include "utils/jsonb.h"
#include "utils/jsonfuncs.h"
+#include "utils/jsonpath.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/timestamp.h"
@@ -181,6 +181,10 @@ static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate
AggStatePerGroup pergroup,
ExprContext *aggcontext,
int setno);
+static void ExecPrepareJsonItemCoercion(JsonbValue *item, JsonExprState *jsestate,
+ bool throw_error,
+ int *jump_eval_item_coercion,
+ Datum *resvalue, bool *resnull);
/*
* ScalarArrayOpExprHashEntry
@@ -482,6 +486,9 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
&&CASE_EEOP_XMLEXPR,
&&CASE_EEOP_JSON_CONSTRUCTOR,
&&CASE_EEOP_IS_JSON,
+ &&CASE_EEOP_JSONEXPR_PATH,
+ &&CASE_EEOP_JSONEXPR_COERCION,
+ &&CASE_EEOP_JSONEXPR_COERCION_FINISH,
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
&&CASE_EEOP_WINDOW_FUNC,
@@ -1551,6 +1558,35 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
EEO_NEXT();
}
+ EEO_CASE(EEOP_JSONEXPR_PATH)
+ {
+ JsonExprState *jsestate = op->d.jsonexpr.jsestate;
+
+ /* too complex for an inline implementation */
+ if (!ExecEvalJsonExprPath(state, op, econtext))
+ EEO_JUMP(jsestate->jump_error);
+ else if (jsestate->post_eval.jump_eval_coercion >= 0)
+ EEO_JUMP(jsestate->post_eval.jump_eval_coercion);
+
+ EEO_NEXT();
+ }
+
+ EEO_CASE(EEOP_JSONEXPR_COERCION)
+ {
+ /* too complex for an inline implementation */
+ ExecEvalJsonCoercion(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
+ EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH)
+ {
+ /* too complex for an inline implementation */
+ ExecEvalJsonCoercionFinish(state, op);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_AGGREF)
{
/*
@@ -4208,6 +4244,335 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
*op->resvalue = BoolGetDatum(res);
}
+/*
+ * Performs JsonPath{Exists|Query|Value}() for given context item and JSON
+ * path.
+ *
+ * Result is set in *op->resvalue and *op->resnull.
+ *
+ * On return, JsonExprPostEvalState is populated with the following details:
+ * - jump_eval_coercion: step address of coercion to apply to the result
+ * - error.value: true if an error occurred during JsonPath evaluation
+ * - empty.value: true if JsonPath{Query|Value}() found no matching item
+ *
+ * No return if the ON ERROR/EMPTY behavior is ERROR.
+ */
+bool
+ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ JsonExprState *jsestate = op->d.jsonexpr.jsestate;
+ JsonExprPostEvalState *post_eval = &jsestate->post_eval;
+ JsonExpr *jexpr = jsestate->jsexpr;
+ Datum item;
+ JsonPath *path;
+ bool throw_error = (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR);
+ bool error = false,
+ empty = false;
+
+ item = jsestate->formatted_expr.value;
+ path = DatumGetJsonPathP(jsestate->pathspec.value);
+
+ /* Reset JsonExprPostEvalState for this evaluation. */
+ memset(post_eval, 0, sizeof(*post_eval));
+ switch (jexpr->op)
+ {
+ case JSON_EXISTS_OP:
+ {
+ bool exists = JsonPathExists(item, path,
+ !throw_error ? &error : NULL,
+ jsestate->args);
+
+ post_eval->jump_eval_coercion = jsestate->jump_eval_result_coercion;
+ if (!error)
+ {
+ *op->resvalue = BoolGetDatum(exists);
+ *op->resnull = false;
+ }
+ }
+ break;
+
+ case JSON_QUERY_OP:
+ *op->resvalue = JsonPathQuery(item, path, jexpr->wrapper, &empty,
+ !throw_error ? &error : NULL,
+ jsestate->args);
+
+ post_eval->jump_eval_coercion = jsestate->jump_eval_result_coercion;
+ if (!error && !empty)
+ *op->resnull = (DatumGetPointer(*op->resvalue) == NULL);
+ break;
+
+ case JSON_VALUE_OP:
+ {
+ JsonbValue *jbv = JsonPathValue(item, path, &empty,
+ !throw_error ? &error : NULL,
+ jsestate->args);
+
+ /* Might get overridden by an item coercion below. */
+ post_eval->jump_eval_coercion = jsestate->jump_eval_result_coercion;
+ if (jbv == NULL)
+ {
+ /* Will be coerced with result_coercion. */
+ *op->resvalue = (Datum) 0;
+ *op->resnull = true;
+ }
+ else if (!error && !empty)
+ {
+ Assert(jbv != NULL);
+
+ /*
+ * If the requested output type is json(b), use
+ * result_coercion to do the coercion.
+ */
+ if (jexpr->returning->typid == JSONOID ||
+ jexpr->returning->typid == JSONBOID)
+ {
+ *op->resvalue = JsonbPGetDatum(JsonbValueToJsonb(jbv));
+ *op->resnull = false;
+ }
+ else
+ {
+ /*
+ * Else, use one of the item_coercions.
+ *
+ * Error out if no cast expression exists.
+ */
+ ExecPrepareJsonItemCoercion(jbv, jsestate, throw_error,
+ &post_eval->jump_eval_coercion,
+ op->resvalue, op->resnull);
+ }
+ }
+ break;
+ }
+
+ default:
+ elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op);
+ return false;
+ }
+
+ if (error)
+ {
+ Assert(!throw_error && jsestate->jump_error >= 0);
+ *op->resvalue = (Datum) 0;
+ *op->resnull = true;
+ post_eval->error.value = BoolGetDatum(true);
+ return false;
+ }
+
+ if (empty)
+ {
+ if (jexpr->on_empty)
+ {
+ if (jexpr->on_empty->btype == JSON_BEHAVIOR_ERROR)
+ ereport(ERROR,
+ (errcode(ERRCODE_NO_SQL_JSON_ITEM),
+ errmsg("no SQL/JSON item")));
+ else
+ post_eval->empty.value = BoolGetDatum(true);
+ }
+ else if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR)
+ ereport(ERROR,
+ (errcode(ERRCODE_NO_SQL_JSON_ITEM),
+ errmsg("no SQL/JSON item")));
+ else
+ post_eval->error.value = BoolGetDatum(true);
+
+ *op->resvalue = (Datum) 0;
+ *op->resnull = true;
+
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Selects a coercion for a given JsonbValue based on its type.
+ *
+ * On return, *resvalue and *resnull are set to the value extracted from the
+ * JsonbValue and *jump_eval_item_coercion is set to the step address of the
+ * coercion expression.
+ *
+ * If the found expression is a JsonCoercion node that means the parser
+ * didnt' find a cast to do the coercion, so throw an error if the
+ * ON ERROR behavior says to do so.
+ */
+static void
+ExecPrepareJsonItemCoercion(JsonbValue *item, JsonExprState *jsestate,
+ bool throw_error,
+ int *jump_eval_item_coercion,
+ Datum *resvalue, bool *resnull)
+{
+ int *eval_item_coercion_jumps = jsestate->eval_item_coercion_jumps;
+ bool *item_coercion_via_expr = jsestate->item_coercion_via_expr;
+ bool via_expr;
+ int jump_to;
+ JsonbValue buf;
+
+ if (item->type == jbvBinary && JsonContainerIsScalar(item->val.binary.data))
+ {
+ bool is_scalar PG_USED_FOR_ASSERTS_ONLY;
+
+ is_scalar = JsonbExtractScalar(item->val.binary.data, &buf);
+ item = &buf;
+ Assert(is_scalar);
+ }
+
+ *resnull = false;
+
+ /* get coercion state reference and datum of the corresponding SQL type */
+ switch (item->type)
+ {
+ case jbvNull:
+ via_expr = item_coercion_via_expr[JsonItemTypeNull];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeNull];
+ *resvalue = (Datum) 0;
+ *resnull = true;
+ break;
+
+ case jbvString:
+ via_expr = item_coercion_via_expr[JsonItemTypeString];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeString];
+ *resvalue =
+ PointerGetDatum(cstring_to_text_with_len(item->val.string.val,
+ item->val.string.len));
+ break;
+
+ case jbvNumeric:
+ via_expr = item_coercion_via_expr[JsonItemTypeNumeric];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeNumeric];
+ *resvalue = NumericGetDatum(item->val.numeric);
+ break;
+
+ case jbvBool:
+ via_expr = item_coercion_via_expr[JsonItemTypeBoolean];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeBoolean];
+ *resvalue = BoolGetDatum(item->val.boolean);
+ break;
+
+ case jbvDatetime:
+ *resvalue = item->val.datetime.value;
+ switch (item->val.datetime.typid)
+ {
+ case DATEOID:
+ via_expr = item_coercion_via_expr[JsonItemTypeDate];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeDate];
+ break;
+ case TIMEOID:
+ via_expr = item_coercion_via_expr[JsonItemTypeTime];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeTime];
+ break;
+ case TIMETZOID:
+ via_expr = item_coercion_via_expr[JsonItemTypeTimetz];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeTimetz];
+ break;
+ case TIMESTAMPOID:
+ via_expr = item_coercion_via_expr[JsonItemTypeTimestamp];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeTimestamp];
+ break;
+ case TIMESTAMPTZOID:
+ via_expr = item_coercion_via_expr[JsonItemTypeTimestamptz];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeTimestamptz];
+ break;
+ default:
+ elog(ERROR, "unexpected jsonb datetime type oid %u",
+ item->val.datetime.typid);
+ }
+ break;
+
+ case jbvArray:
+ case jbvObject:
+ case jbvBinary:
+ via_expr = item_coercion_via_expr[JsonItemTypeComposite];
+ jump_to = eval_item_coercion_jumps[JsonItemTypeComposite];
+ *resvalue = JsonbPGetDatum(JsonbValueToJsonb(item));
+ break;
+
+ default:
+ elog(ERROR, "unexpected jsonb value type %d", item->type);
+ }
+
+ /* If the expression is a JsonCoercion, throw an error. */
+ if (jump_to >= 0 && !via_expr)
+ {
+ if (throw_error)
+ ereport(ERROR,
+ (errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
+ errmsg("SQL/JSON item cannot be cast to target type")));
+
+ *resvalue = (Datum) 0;
+ *resnull = true;
+ }
+
+ *jump_eval_item_coercion = jump_to;
+}
+
+/*
+ * Coerce a jsonb value produced by ExecEvalJsonExprPath() or an ON ERROR /
+ * EMPTY behavior expression to the target type by either calling
+ * json_populate_type() or the type's input function.
+ *
+ * If a soft-error occurs, it will be checked by EEOP_JSONEXPR_COECION_FINISH
+ * that will run right after this.
+ */
+void
+ExecEvalJsonCoercion(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext)
+{
+ JsonCoercionState *jcstate = op->d.jsonexpr_coercion.jcstate;
+ JsonCoercion *coercion = jcstate->coercion;
+ Datum res = *op->resvalue;
+ bool resnull = *op->resnull;
+
+ if (coercion->via_populate)
+ {
+ *op->resvalue = json_populate_type(res, JSONBOID,
+ coercion->targettype,
+ coercion->targettypmod,
+ &jcstate->cache,
+ econtext->ecxt_per_query_memory,
+ op->resnull,
+ (Node *) jcstate->escontext);
+ }
+ else if (coercion->via_io)
+ {
+ char *val_string = resnull ? NULL :
+ JsonbUnquote(DatumGetJsonbP(res));
+
+ (void) InputFunctionCallSafe(jcstate->input.finfo, val_string,
+ jcstate->input.typioparam,
+ coercion->targettypmod,
+ (Node *) jcstate->escontext,
+ op->resvalue);
+ }
+}
+
+/*
+ * Checks if the coercion evaluation led to an error. If an error did occur,
+ * this sets post_eval->error to trigger the subsequent ON ERROR handling
+ * steps.
+ */
+void
+ExecEvalJsonCoercionFinish(ExprState *state, ExprEvalStep *op)
+{
+ JsonExprState *jsestate = op->d.jsonexpr.jsestate;
+
+ if (SOFT_ERROR_OCCURRED(&jsestate->escontext))
+ {
+ *op->resvalue = (Datum) 0;
+ *op->resnull = true;
+ jsestate->post_eval.error.value = BoolGetDatum(true);
+
+ /*
+ * Also make ErrorSaveContext ready for the next row. Since we never
+ * set details_wanted, we don't need to also reset error_data, which
+ * would be NULL anyway.
+ */
+ Assert(!jsestate->escontext.details_wanted &&
+ jsestate->escontext.error_data == NULL);
+ jsestate->escontext.error_occurred = false;
+ }
+}
/*
* ExecEvalGroupingFunc
diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c
index 2c8ac02550..4e83e5ef7f 100644
--- a/src/backend/jit/llvm/llvmjit.c
+++ b/src/backend/jit/llvm/llvmjit.c
@@ -84,6 +84,7 @@ LLVMTypeRef StructAggState;
LLVMTypeRef StructAggStatePerGroupData;
LLVMTypeRef StructAggStatePerTransData;
LLVMTypeRef StructPlanState;
+LLVMTypeRef StructJsonExprPostEvalState;
LLVMValueRef AttributeTemplate;
LLVMValueRef ExecEvalSubroutineTemplate;
@@ -1186,6 +1187,7 @@ llvm_create_types(void)
StructAggStatePerTransData = llvm_pg_var_type("StructAggStatePerTransData");
StructPlanState = llvm_pg_var_type("StructPlanState");
StructMinimalTupleData = llvm_pg_var_type("StructMinimalTupleData");
+ StructJsonExprPostEvalState = llvm_pg_var_type("StructJsonExprPostEvalState");
AttributeTemplate = LLVMGetNamedFunction(llvm_types_module, "AttributeTemplate");
ExecEvalSubroutineTemplate = LLVMGetNamedFunction(llvm_types_module, "ExecEvalSubroutineTemplate");
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 81856a9dc7..5c67d7749b 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1930,6 +1930,146 @@ llvm_compile_expr(ExprState *state)
LLVMBuildBr(b, opblocks[opno + 1]);
break;
+ case EEOP_JSONEXPR_PATH:
+ {
+ JsonExprState *jsestate = op->d.jsonexpr.jsestate;
+ LLVMValueRef v_ret;
+ LLVMBasicBlockRef b_coercion;
+
+ /*
+ * Call ExecEvalJsonExprPath(). It returns false when
+ * an error occurs or if the no match is found, which
+ * is handled by jumping to the block at
+ * jsestate->jump_error. If true is returned, jump
+ * to the block that coerces the result value.
+ */
+ v_ret = build_EvalXFunc(b, mod, "ExecEvalJsonExprPath",
+ v_state, op, v_econtext);
+ v_ret = LLVMBuildZExt(b, v_ret, TypeStorageBool, "");
+
+ b_coercion =
+ l_bb_before_v(opblocks[opno + 1],
+ "op.%d.jsonexpr_coercion", opno);
+ LLVMBuildCondBr(b,
+ LLVMBuildICmp(b,
+ LLVMIntEQ,
+ v_ret,
+ l_sbool_const(0),
+ ""),
+ jsestate->jump_error >= 0 ?
+ opblocks[jsestate->jump_error] :
+ b_coercion,
+ b_coercion);
+
+ /*
+ * Build a switch to map
+ * JsonExprPostEvalState.jump_eval_coercion, which is a
+ * runtime value of the step address of the coercion
+ * expression set by ExecEvalJsonExprPath() based on the
+ * result value it computes, to either
+ * jsestate->jump_eval_result_coercion or one of those in
+ * the jsestate->eval_item_coercion_jumps[] array.
+ */
+ LLVMPositionBuilderAtEnd(b, b_coercion);
+ if (jsestate->jump_eval_result_coercion >= 0 ||
+ jsestate->num_item_coercions > 0)
+ {
+ JsonExprPostEvalState *post_eval = &jsestate->post_eval;
+ int i;
+ LLVMValueRef v_post_eval;
+ LLVMValueRef v_post_eval_jump_eval_coercionp;
+ LLVMValueRef v_post_eval_jump_eval_coercion;
+ LLVMValueRef v_jump_coercion;
+ LLVMValueRef v_switch;
+ LLVMBasicBlockRef b_done,
+ b_result_coercion_block,
+ *b_item_coercion_blocks = NULL;
+
+ v_post_eval = l_ptr_const(post_eval, l_ptr(StructJsonExprPostEvalState));
+ v_post_eval_jump_eval_coercionp =
+ l_struct_gep(b,
+ StructJsonExprPostEvalState,
+ v_post_eval,
+ FIELDNO_JSONEXPRPOSTEVALSTATE_JUMP_EVAL_COERCION,
+ "v_post_eval_jump_eval_coercion");
+ v_post_eval_jump_eval_coercion =
+ l_load(b, LLVMInt32TypeInContext(lc),
+ v_post_eval_jump_eval_coercionp, "");
+
+ b_result_coercion_block =
+ l_bb_before_v(opblocks[opno + 1],
+ "op.%d.jsonexpr_result_coercion", opno);
+ if (jsestate->num_item_coercions > 0)
+ {
+ b_item_coercion_blocks = palloc(sizeof(LLVMBasicBlockRef) *
+ jsestate->num_item_coercions);
+ for (i = 0; i < jsestate->num_item_coercions; i++)
+ {
+ b_item_coercion_blocks[i] =
+ l_bb_before_v(opblocks[opno + 1],
+ "op.%d.jsonexpr_item_coercion.%d",
+ opno, i);
+ }
+ }
+ b_done =
+ l_bb_before_v(opblocks[opno + 1],
+ "op.%d.jsonexpr_done", opno);
+
+ v_switch = LLVMBuildSwitch(b,
+ v_post_eval_jump_eval_coercion,
+ b_done,
+ jsestate->num_item_coercions + 1);
+ if (jsestate->jump_eval_result_coercion >= 0)
+ {
+ v_jump_coercion = l_int32_const(lc, jsestate->jump_eval_result_coercion);
+ LLVMAddCase(v_switch, v_jump_coercion, b_result_coercion_block);
+ }
+
+ for (i = 0; i < jsestate->num_item_coercions; i++)
+ {
+ if (jsestate->eval_item_coercion_jumps[i] >= 0)
+ {
+ v_jump_coercion = l_int32_const(lc, jsestate->eval_item_coercion_jumps[i]);
+ LLVMAddCase(v_switch, v_jump_coercion, b_item_coercion_blocks[i]);
+ }
+ }
+
+ LLVMPositionBuilderAtEnd(b, b_result_coercion_block);
+ if (jsestate->jump_eval_result_coercion >= 0)
+ LLVMBuildBr(b, opblocks[jsestate->jump_eval_result_coercion]);
+ else
+ LLVMBuildUnreachable(b);
+
+ for (i = 0; i < jsestate->num_item_coercions; i++)
+ {
+ LLVMPositionBuilderAtEnd(b, b_item_coercion_blocks[i]);
+ if (jsestate->eval_item_coercion_jumps[i] >= 0)
+ LLVMBuildBr(b, opblocks[jsestate->eval_item_coercion_jumps[i]]);
+ else
+ LLVMBuildUnreachable(b);
+ }
+
+ LLVMPositionBuilderAtEnd(b, b_done);
+ }
+
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+ }
+
+ case EEOP_JSONEXPR_COERCION:
+ build_EvalXFunc(b, mod, "ExecEvalJsonCoercion",
+ v_state, op, v_econtext);
+
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
+ case EEOP_JSONEXPR_COERCION_FINISH:
+ build_EvalXFunc(b, mod, "ExecEvalJsonCoercionFinish",
+ v_state, op);
+
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_AGGREF:
{
LLVMValueRef v_aggno;
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index 3a4be09e50..1546037b3a 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -69,6 +69,7 @@ MinimalTupleTableSlot StructMinimalTupleTableSlot;
TupleDescData StructTupleDescData;
PlanState StructPlanState;
MinimalTupleData StructMinimalTupleData;
+JsonExprPostEvalState StructJsonExprPostEvalState;
/*
@@ -172,6 +173,9 @@ void *referenced_functions[] =
ExecEvalXmlExpr,
ExecEvalJsonConstructor,
ExecEvalJsonIsPredicate,
+ ExecEvalJsonCoercion,
+ ExecEvalJsonCoercionFinish,
+ ExecEvalJsonExprPath,
MakeExpandedObjectReadOnlyInternal,
slot_getmissingattrs,
slot_getsomeattrs_int,
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index c6fb571982..e466fe5176 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -857,6 +857,24 @@ makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr,
return jve;
}
+/*
+ * makeJsonBehavior -
+ * creates a JsonBehavior node
+ */
+JsonBehavior *
+makeJsonBehavior(JsonBehaviorType type, Node *expr, JsonCoercion *coercion,
+ int location)
+{
+ JsonBehavior *behavior = makeNode(JsonBehavior);
+
+ behavior->btype = type;
+ behavior->expr = expr;
+ behavior->coercion = coercion;
+ behavior->location = location;
+
+ return behavior;
+}
+
/*
* makeJsonEncoding -
* converts JSON encoding name to enum JsonEncoding
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c03f4f23e2..62bf08ad27 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -234,6 +234,33 @@ exprType(const Node *expr)
case T_JsonIsPredicate:
type = BOOLOID;
break;
+ case T_JsonExpr:
+ {
+ const JsonExpr *jexpr = (const JsonExpr *) expr;
+
+ type = jexpr->returning->typid;
+ break;
+ }
+ case T_JsonCoercion:
+ {
+ const JsonCoercion *coercion = (const JsonCoercion *) expr;
+
+ type = coercion->targettype;
+ break;
+ }
+ case T_JsonItemCoercion:
+ type = exprType(((JsonItemCoercion *) expr)->coercion);
+ break;
+ case T_JsonBehavior:
+ {
+ const JsonBehavior *behavior = (const JsonBehavior *) expr;
+
+ if (behavior->coercion)
+ type = exprType((Node *) behavior->coercion);
+ else
+ type = exprType(behavior->expr);
+ break;
+ }
case T_NullTest:
type = BOOLOID;
break;
@@ -491,8 +518,32 @@ exprTypmod(const Node *expr)
return ((const SQLValueFunction *) expr)->typmod;
case T_JsonValueExpr:
return exprTypmod((Node *) ((const JsonValueExpr *) expr)->formatted_expr);
- case T_JsonConstructorExpr:
- return ((const JsonConstructorExpr *) expr)->returning->typmod;
+ case T_JsonExpr:
+ {
+ const JsonExpr *jexpr = (const JsonExpr *) expr;
+
+ return jexpr->returning->typmod;
+ }
+ break;
+ case T_JsonCoercion:
+ {
+ const JsonCoercion *coercion = (const JsonCoercion *) expr;
+
+ return coercion->targettypmod;
+ }
+ break;
+ case T_JsonItemCoercion:
+ return exprTypmod(((JsonItemCoercion *) expr)->coercion);
+ case T_JsonBehavior:
+ {
+ const JsonBehavior *behavior = (const JsonBehavior *) expr;
+
+ if (behavior->coercion)
+ return exprTypmod((Node *) behavior->coercion);
+ else
+ return exprTypmod(behavior->expr);
+ }
+ break;
case T_CoerceToDomain:
return ((const CoerceToDomain *) expr)->resulttypmod;
case T_CoerceToDomainValue:
@@ -969,6 +1020,34 @@ exprCollation(const Node *expr)
/* IS JSON's result is boolean ... */
coll = InvalidOid; /* ... so it has no collation */
break;
+ case T_JsonExpr:
+ coll = exprCollation(((JsonExpr *) expr)->result_coercion);
+ break;
+ case T_JsonCoercion:
+ {
+ JsonCoercion *coercion = (JsonCoercion *) expr;
+
+ if (coercion->via_io || coercion->via_populate)
+ coll = coercion->collation;
+ else
+ coll = InvalidOid;
+ }
+ break;
+ case T_JsonItemCoercion:
+ coll = exprCollation(((JsonItemCoercion *) expr)->coercion);
+ break;
+ case T_JsonBehavior:
+ {
+ JsonBehavior *behavior = (JsonBehavior *) expr;
+
+ if (behavior->coercion)
+ coll = exprCollation((Node *) behavior->coercion);
+ else if (behavior->expr)
+ coll = exprCollation(behavior->expr);
+ else
+ coll = InvalidOid;
+ }
+ break;
case T_NullTest:
/* NullTest's result is boolean ... */
coll = InvalidOid; /* ... so it has no collation */
@@ -1205,6 +1284,42 @@ exprSetCollation(Node *expr, Oid collation)
case T_JsonIsPredicate:
Assert(!OidIsValid(collation)); /* result is always boolean */
break;
+ case T_JsonExpr:
+ {
+ JsonExpr *jexpr = (JsonExpr *) expr;
+
+ if (jexpr->result_coercion)
+ exprSetCollation((Node *) jexpr->result_coercion, collation);
+ else
+ Assert(!OidIsValid(collation)); /* result is always a
+ * json[b] type */
+ }
+ break;
+ case T_JsonItemCoercion:
+ {
+ JsonItemCoercion *item_coercion = (JsonItemCoercion *) expr;
+
+ if (item_coercion->coercion)
+ exprSetCollation(item_coercion->coercion, collation);
+ }
+ break;
+ case T_JsonCoercion:
+ {
+ JsonCoercion *coercion = (JsonCoercion *) expr;
+
+ coercion->collation = collation;
+ }
+ break;
+ case T_JsonBehavior:
+ {
+ JsonBehavior *behavior = (JsonBehavior *) expr;
+
+ if (behavior->expr)
+ exprSetCollation(behavior->expr, collation);
+ if (behavior->coercion)
+ exprSetCollation((Node *) behavior->coercion, collation);
+ }
+ break;
case T_NullTest:
/* NullTest's result is boolean ... */
Assert(!OidIsValid(collation)); /* ... so never set a collation */
@@ -1508,6 +1623,18 @@ exprLocation(const Node *expr)
case T_JsonIsPredicate:
loc = ((const JsonIsPredicate *) expr)->location;
break;
+ case T_JsonExpr:
+ {
+ const JsonExpr *jsexpr = (const JsonExpr *) expr;
+
+ /* consider both function name and leftmost arg */
+ loc = leftmostLoc(jsexpr->location,
+ exprLocation(jsexpr->formatted_expr));
+ }
+ break;
+ case T_JsonBehavior:
+ loc = exprLocation(((JsonBehavior *) expr)->expr);
+ break;
case T_NullTest:
{
const NullTest *nexpr = (const NullTest *) expr;
@@ -2260,6 +2387,45 @@ expression_tree_walker_impl(Node *node,
break;
case T_JsonIsPredicate:
return WALK(((JsonIsPredicate *) node)->expr);
+ case T_JsonExpr:
+ {
+ JsonExpr *jexpr = (JsonExpr *) node;
+
+ if (WALK(jexpr->formatted_expr))
+ return true;
+ if (WALK(jexpr->result_coercion))
+ return true;
+ if (WALK(jexpr->item_coercions))
+ return true;
+ if (WALK(jexpr->passing_values))
+ return true;
+ /* we assume walker doesn't care about passing_names */
+ if (WALK(jexpr->on_empty))
+ return true;
+ if (WALK(jexpr->on_error))
+ return true;
+ }
+ break;
+ case T_JsonCoercion:
+ break;
+ case T_JsonItemCoercion:
+ {
+ JsonItemCoercion *item_coercion = (JsonItemCoercion *) node;
+
+ if (WALK(item_coercion->coercion))
+ return true;
+ }
+ break;
+ case T_JsonBehavior:
+ {
+ JsonBehavior *behavior = (JsonBehavior *) node;
+
+ if (WALK(behavior->expr))
+ return true;
+ if (WALK(behavior->coercion))
+ return true;
+ }
+ break;
case T_NullTest:
return WALK(((NullTest *) node)->arg);
case T_BooleanTest:
@@ -3259,6 +3425,46 @@ expression_tree_mutator_impl(Node *node,
return (Node *) newnode;
}
+ case T_JsonExpr:
+ {
+ JsonExpr *jexpr = (JsonExpr *) node;
+ JsonExpr *newnode;
+
+ FLATCOPY(newnode, jexpr, JsonExpr);
+ MUTATE(newnode->path_spec, jexpr->path_spec, Node *);
+ MUTATE(newnode->formatted_expr, jexpr->formatted_expr, Node *);
+ MUTATE(newnode->result_coercion, jexpr->result_coercion, Node *);
+ MUTATE(newnode->item_coercions, jexpr->item_coercions, List *);
+ MUTATE(newnode->passing_values, jexpr->passing_values, List *);
+ /* assume mutator does not care about passing_names */
+ MUTATE(newnode->on_empty, jexpr->on_empty, JsonBehavior *);
+ MUTATE(newnode->on_error, jexpr->on_error, JsonBehavior *);
+ return (Node *) newnode;
+ }
+ break;
+ case T_JsonCoercion:
+ return (Node *) copyObject(node);
+ case T_JsonItemCoercion:
+ {
+ JsonItemCoercion *item_coercion = (JsonItemCoercion *) node;
+ JsonItemCoercion *newnode;
+
+ FLATCOPY(newnode, item_coercion, JsonItemCoercion);
+ MUTATE(newnode->coercion, item_coercion->coercion, Node *);
+ return (Node *) newnode;
+ }
+ break;
+ case T_JsonBehavior:
+ {
+ JsonBehavior *behavior = (JsonBehavior *) node;
+ JsonBehavior *newnode;
+
+ FLATCOPY(newnode, behavior, JsonBehavior);
+ MUTATE(newnode->expr, behavior->expr, Node *);
+ MUTATE(newnode->coercion, behavior->coercion, JsonCoercion *);
+ return (Node *) newnode;
+ }
+ break;
case T_NullTest:
{
NullTest *ntest = (NullTest *) node;
@@ -3945,6 +4151,34 @@ raw_expression_tree_walker_impl(Node *node,
break;
case T_JsonIsPredicate:
return WALK(((JsonIsPredicate *) node)->expr);
+ case T_JsonArgument:
+ return WALK(((JsonArgument *) node)->val);
+ case T_JsonFuncExpr:
+ {
+ JsonFuncExpr *jfe = (JsonFuncExpr *) node;
+
+ if (WALK(jfe->context_item))
+ return true;
+ if (WALK(jfe->pathspec))
+ return true;
+ if (WALK(jfe->passing))
+ return true;
+ if (jfe->output && WALK(jfe->output))
+ return true;
+ if (jfe->behavior)
+ return true;
+ }
+ break;
+ case T_JsonBehavior:
+ {
+ JsonBehavior *jb = (JsonBehavior *) node;
+
+ if (WALK(jb->expr))
+ return true;
+ if (WALK(jb->coercion))
+ return true;
+ }
+ break;
case T_NullTest:
return WALK(((NullTest *) node)->arg);
case T_BooleanTest:
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index d6ceafd51c..3bb48b64d5 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -4850,7 +4850,8 @@ cost_qual_eval_walker(Node *node, cost_qual_eval_context *context)
IsA(node, SQLValueFunction) ||
IsA(node, XmlExpr) ||
IsA(node, CoerceToDomain) ||
- IsA(node, NextValueExpr))
+ IsA(node, NextValueExpr) ||
+ IsA(node, JsonExpr))
{
/* Treat all these as having cost 1 */
context->total.per_tuple += cpu_operator_cost;
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 507c101661..06297b0391 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -53,6 +53,7 @@
#include "utils/fmgroids.h"
#include "utils/json.h"
#include "utils/jsonb.h"
+#include "utils/jsonpath.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/syscache.h"
@@ -417,6 +418,24 @@ contain_mutable_functions_walker(Node *node, void *context)
/* Check all subnodes */
}
+ if (IsA(node, JsonExpr))
+ {
+ JsonExpr *jexpr = castNode(JsonExpr, node);
+ Const *cnst;
+
+ if (!IsA(jexpr->path_spec, Const))
+ return true;
+
+ cnst = castNode(Const, jexpr->path_spec);
+
+ Assert(cnst->consttype == JSONPATHOID);
+ if (cnst->constisnull)
+ return false;
+
+ return jspIsMutable(DatumGetJsonPathP(cnst->constvalue),
+ jexpr->passing_names, jexpr->passing_values);
+ }
+
if (IsA(node, SQLValueFunction))
{
/* all variants of SQLValueFunction are stable */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d631ac89a9..4f92d000ec 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -650,11 +650,18 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
json_returning_clause_opt
json_name_and_value
json_aggregate_func
+ json_argument
+ json_behavior
%type <list> json_name_and_value_list
json_value_expr_list
json_array_aggregate_order_by_clause_opt
+ json_arguments
+ json_behavior_clause_opt
+ json_passing_clause_opt
%type <ival> json_encoding_clause_opt
json_predicate_type_constraint
+ json_quotes_clause_opt
+ json_wrapper_behavior
%type <boolean> json_key_uniqueness_constraint_opt
json_object_constructor_null_clause_opt
json_array_constructor_null_clause_opt
@@ -695,7 +702,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
- COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
+ COMMITTED COMPRESSION CONCURRENTLY CONDITIONAL CONFIGURATION CONFLICT
CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
COST CREATE CROSS CSV CUBE CURRENT_P
CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA
@@ -706,8 +713,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
DOUBLE_P DROP
- EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
- EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+ EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
+ EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
EXTENSION EXTERNAL EXTRACT
FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -722,10 +729,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
- JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_OBJECT JSON_OBJECTAGG
- JSON_SCALAR JSON_SERIALIZE
+ JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG
+ JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE
- KEY KEYS
+ KEEP KEY KEYS
LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
@@ -739,7 +746,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
NULLS_P NUMERIC
- OBJECT_P OF OFF OFFSET OIDS OLD ON ONLY OPERATOR OPTION OPTIONS OR
+ OBJECT_P OF OFF OFFSET OIDS OLD OMIT ON ONLY OPERATOR OPTION OPTIONS OR
ORDER ORDINALITY OTHERS OUT_P OUTER_P
OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
@@ -748,7 +755,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
- QUOTE
+ QUOTE QUOTES
RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING
REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
@@ -759,7 +766,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
SEQUENCE SEQUENCES
SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
- START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
+ START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P
SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
@@ -767,7 +774,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
TREAT TRIGGER TRIM TRUE_P
TRUNCATE TRUSTED TYPE_P TYPES_P
- UESCAPE UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
+ UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN
UNLISTEN UNLOGGED UNTIL UPDATE USER USING
VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
@@ -15768,6 +15775,60 @@ func_expr_common_subexpr:
n->location = @1;
$$ = (Node *) n;
}
+ | JSON_QUERY '('
+ json_value_expr ',' a_expr json_passing_clause_opt
+ json_returning_clause_opt
+ json_wrapper_behavior
+ json_quotes_clause_opt
+ json_behavior_clause_opt
+ ')'
+ {
+ JsonFuncExpr *n = makeNode(JsonFuncExpr);
+
+ n->op = JSON_QUERY_OP;
+ n->context_item = (JsonValueExpr *) $3;
+ n->pathspec = $5;
+ n->passing = $6;
+ n->output = (JsonOutput *) $7;
+ n->wrapper = $8;
+ n->quotes = $9;
+ n->behavior = $10;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | JSON_EXISTS '('
+ json_value_expr ',' a_expr json_passing_clause_opt
+ json_behavior_clause_opt
+ ')'
+ {
+ JsonFuncExpr *n = makeNode(JsonFuncExpr);
+
+ n->op = JSON_EXISTS_OP;
+ n->context_item = (JsonValueExpr *) $3;
+ n->pathspec = $5;
+ n->passing = $6;
+ n->output = NULL;
+ n->behavior = $7;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
+ | JSON_VALUE '('
+ json_value_expr ',' a_expr json_passing_clause_opt
+ json_returning_clause_opt
+ json_behavior_clause_opt
+ ')'
+ {
+ JsonFuncExpr *n = makeNode(JsonFuncExpr);
+
+ n->op = JSON_VALUE_OP;
+ n->context_item = (JsonValueExpr *) $3;
+ n->pathspec = $5;
+ n->passing = $6;
+ n->output = (JsonOutput *) $7;
+ n->behavior = $8;
+ n->location = @1;
+ $$ = (Node *) n;
+ }
;
@@ -16494,6 +16555,27 @@ opt_asymmetric: ASYMMETRIC
;
/* SQL/JSON support */
+json_passing_clause_opt:
+ PASSING json_arguments { $$ = $2; }
+ | /*EMPTY*/ { $$ = NIL; }
+ ;
+
+json_arguments:
+ json_argument { $$ = list_make1($1); }
+ | json_arguments ',' json_argument { $$ = lappend($1, $3); }
+ ;
+
+json_argument:
+ json_value_expr AS ColLabel
+ {
+ JsonArgument *n = makeNode(JsonArgument);
+
+ n->val = (JsonValueExpr *) $1;
+ n->name = $3;
+ $$ = (Node *) n;
+ }
+ ;
+
json_value_expr:
a_expr json_format_clause_opt
{
@@ -16519,6 +16601,27 @@ json_encoding_clause_opt:
| /* EMPTY */ { $$ = JS_ENC_DEFAULT; }
;
+/* ARRAY is a noise word */
+json_wrapper_behavior:
+ WITHOUT WRAPPER { $$ = JSW_NONE; }
+ | WITHOUT ARRAY WRAPPER { $$ = JSW_NONE; }
+ | WITH WRAPPER { $$ = JSW_UNCONDITIONAL; }
+ | WITH ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; }
+ | WITH CONDITIONAL ARRAY WRAPPER { $$ = JSW_CONDITIONAL; }
+ | WITH UNCONDITIONAL ARRAY WRAPPER { $$ = JSW_UNCONDITIONAL; }
+ | WITH CONDITIONAL WRAPPER { $$ = JSW_CONDITIONAL; }
+ | WITH UNCONDITIONAL WRAPPER { $$ = JSW_UNCONDITIONAL; }
+ | /* empty */ { $$ = JSW_NONE; }
+ ;
+
+json_quotes_clause_opt:
+ KEEP QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_KEEP; }
+ | KEEP QUOTES { $$ = JS_QUOTES_KEEP; }
+ | OMIT QUOTES ON SCALAR STRING_P { $$ = JS_QUOTES_OMIT; }
+ | OMIT QUOTES { $$ = JS_QUOTES_OMIT; }
+ | /* EMPTY */ { $$ = JS_QUOTES_UNSPEC; }
+ ;
+
json_returning_clause_opt:
RETURNING Typename json_format_clause_opt
{
@@ -16532,6 +16635,39 @@ json_returning_clause_opt:
| /* EMPTY */ { $$ = NULL; }
;
+json_behavior:
+ DEFAULT a_expr
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_DEFAULT, $2, NULL, @1); }
+ | ERROR_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, NULL, @1); }
+ | NULL_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_NULL, NULL, NULL, @1); }
+ | TRUE_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_TRUE, NULL, NULL, @1); }
+ | FALSE_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_FALSE, NULL, NULL, @1); }
+ | UNKNOWN
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL, NULL, @1); }
+ | EMPTY_P ARRAY
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, NULL, @1); }
+ | EMPTY_P OBJECT_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL, NULL, @1); }
+ /* non-standard, for Oracle compatibility only */
+ | EMPTY_P
+ { $$ = (Node *) makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL, NULL, @1); }
+ ;
+
+json_behavior_clause_opt:
+ json_behavior ON EMPTY_P
+ { $$ = list_make2($1, NULL); }
+ | json_behavior ON ERROR_P
+ { $$ = list_make2(NULL, $1); }
+ | json_behavior ON EMPTY_P json_behavior ON ERROR_P
+ { $$ = list_make2($1, $4); }
+ | /* EMPTY */
+ { $$ = list_make2(NULL, NULL); }
+ ;
+
/*
* We must assign the only-JSON production a precedence less than IDENT in
* order to favor shifting over reduction when JSON is followed by VALUE_P,
@@ -17135,6 +17271,7 @@ unreserved_keyword:
| COMMIT
| COMMITTED
| COMPRESSION
+ | CONDITIONAL
| CONFIGURATION
| CONFLICT
| CONNECTION
@@ -17171,10 +17308,12 @@ unreserved_keyword:
| DOUBLE_P
| DROP
| EACH
+ | EMPTY_P
| ENABLE_P
| ENCODING
| ENCRYPTED
| ENUM_P
+ | ERROR_P
| ESCAPE
| EVENT
| EXCLUDE
@@ -17224,6 +17363,7 @@ unreserved_keyword:
| INSTEAD
| INVOKER
| ISOLATION
+ | KEEP
| KEY
| KEYS
| LABEL
@@ -17270,6 +17410,7 @@ unreserved_keyword:
| OFF
| OIDS
| OLD
+ | OMIT
| OPERATOR
| OPTION
| OPTIONS
@@ -17300,6 +17441,7 @@ unreserved_keyword:
| PROGRAM
| PUBLICATION
| QUOTE
+ | QUOTES
| RANGE
| READ
| REASSIGN
@@ -17359,6 +17501,7 @@ unreserved_keyword:
| STORAGE
| STORED
| STRICT_P
+ | STRING_P
| STRIP_P
| SUBSCRIPTION
| SUPPORT
@@ -17381,6 +17524,7 @@ unreserved_keyword:
| UESCAPE
| UNBOUNDED
| UNCOMMITTED
+ | UNCONDITIONAL
| UNENCRYPTED
| UNKNOWN
| UNLISTEN
@@ -17441,10 +17585,13 @@ col_name_keyword:
| JSON
| JSON_ARRAY
| JSON_ARRAYAGG
+ | JSON_EXISTS
| JSON_OBJECT
| JSON_OBJECTAGG
+ | JSON_QUERY
| JSON_SCALAR
| JSON_SERIALIZE
+ | JSON_VALUE
| LEAST
| NATIONAL
| NCHAR
@@ -17677,6 +17824,7 @@ bare_label_keyword:
| COMMITTED
| COMPRESSION
| CONCURRENTLY
+ | CONDITIONAL
| CONFIGURATION
| CONFLICT
| CONNECTION
@@ -17729,11 +17877,13 @@ bare_label_keyword:
| DROP
| EACH
| ELSE
+ | EMPTY_P
| ENABLE_P
| ENCODING
| ENCRYPTED
| END_P
| ENUM_P
+ | ERROR_P
| ESCAPE
| EVENT
| EXCLUDE
@@ -17803,10 +17953,14 @@ bare_label_keyword:
| JSON
| JSON_ARRAY
| JSON_ARRAYAGG
+ | JSON_EXISTS
| JSON_OBJECT
| JSON_OBJECTAGG
+ | JSON_QUERY
| JSON_SCALAR
| JSON_SERIALIZE
+ | JSON_VALUE
+ | KEEP
| KEY
| KEYS
| LABEL
@@ -17867,6 +18021,7 @@ bare_label_keyword:
| OFF
| OIDS
| OLD
+ | OMIT
| ONLY
| OPERATOR
| OPTION
@@ -17904,6 +18059,7 @@ bare_label_keyword:
| PROGRAM
| PUBLICATION
| QUOTE
+ | QUOTES
| RANGE
| READ
| REAL
@@ -17972,6 +18128,7 @@ bare_label_keyword:
| STORAGE
| STORED
| STRICT_P
+ | STRING_P
| STRIP_P
| SUBSCRIPTION
| SUBSTRING
@@ -18006,6 +18163,7 @@ bare_label_keyword:
| UESCAPE
| UNBOUNDED
| UNCOMMITTED
+ | UNCONDITIONAL
| UNENCRYPTED
| UNIQUE
| UNKNOWN
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 64c582c344..55a7680001 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -37,6 +37,7 @@
#include "utils/builtins.h"
#include "utils/date.h"
#include "utils/fmgroids.h"
+#include "utils/jsonb.h"
#include "utils/lsyscache.h"
#include "utils/timestamp.h"
#include "utils/xml.h"
@@ -90,6 +91,21 @@ static Node *transformJsonParseExpr(ParseState *pstate, JsonParseExpr *expr);
static Node *transformJsonScalarExpr(ParseState *pstate, JsonScalarExpr *expr);
static Node *transformJsonSerializeExpr(ParseState *pstate,
JsonSerializeExpr *expr);
+static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *p);
+static JsonExpr *transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func,
+ const char *constructName);
+static void transformJsonPassingArgs(ParseState *pstate, const char *constructName,
+ JsonFormatType format, List *args,
+ List **passing_values, List **passing_names);
+static Node *coerceJsonFuncExprOutput(ParseState *pstate, JsonExpr *jsexpr);
+static Oid JsonFuncExprDefaultReturnType(JsonExpr *jsexpr);
+static Node *coerceJsonExpr(ParseState *pstate, Node *expr,
+ const JsonReturning *returning);
+static List *InitJsonItemCoercions(ParseState *pstate, const JsonReturning *returning,
+ Oid contextItemTypeId);
+static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
+ JsonBehaviorType default_behavior,
+ JsonReturning *returning);
static Node *make_row_comparison_op(ParseState *pstate, List *opname,
List *largs, List *rargs, int location);
static Node *make_row_distinct_op(ParseState *pstate, List *opname,
@@ -353,6 +369,10 @@ transformExprRecurse(ParseState *pstate, Node *expr)
result = transformJsonSerializeExpr(pstate, (JsonSerializeExpr *) expr);
break;
+ case T_JsonFuncExpr:
+ result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr);
+ break;
+
default:
/* should not reach here */
elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -3229,7 +3249,7 @@ makeJsonByteaToTextConversion(Node *expr, JsonFormat *format, int location)
static Node *
transformJsonValueExpr(ParseState *pstate, const char *constructName,
JsonValueExpr *ve, JsonFormatType default_format,
- Oid targettype)
+ Oid targettype, bool isarg)
{
Node *expr = transformExprRecurse(pstate, (Node *) ve->raw_expr);
Node *rawexpr;
@@ -3261,6 +3281,41 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName,
else
format = ve->format->format_type;
}
+ else if (isarg)
+ {
+ /*
+ * Special treatment for PASSING arguments.
+ *
+ * Pass types supported by GetJsonPathVar() /
+ * JsonItemFromDatum() directly without converting to json[b].
+ */
+ switch (exprtype)
+ {
+ case BOOLOID:
+ case NUMERICOID:
+ case INT2OID:
+ case INT4OID:
+ case INT8OID:
+ case FLOAT4OID:
+ case FLOAT8OID:
+ case TEXTOID:
+ case VARCHAROID:
+ case DATEOID:
+ case TIMEOID:
+ case TIMETZOID:
+ case TIMESTAMPOID:
+ case TIMESTAMPTZOID:
+ return expr;
+
+ default:
+ if (typcategory == TYPCATEGORY_STRING)
+ return expr;
+ /* else convert argument to json[b] type */
+ break;
+ }
+
+ format = default_format;
+ }
else if (exprtype == JSONOID || exprtype == JSONBOID)
format = JS_FORMAT_DEFAULT; /* do not format json[b] types */
else
@@ -3272,7 +3327,12 @@ transformJsonValueExpr(ParseState *pstate, const char *constructName,
Node *coerced;
bool only_allow_cast = OidIsValid(targettype);
- if (!only_allow_cast &&
+ /*
+ * PASSING args are handled appropriately by GetJsonPathVar() /
+ * JsonItemFromDatum().
+ */
+ if (!isarg &&
+ !only_allow_cast &&
exprtype != BYTEAOID && typcategory != TYPCATEGORY_STRING)
ereport(ERROR,
errcode(ERRCODE_DATATYPE_MISMATCH),
@@ -3425,6 +3485,11 @@ transformJsonOutput(ParseState *pstate, const JsonOutput *output,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("returning SETOF types is not supported in SQL/JSON functions"));
+ if (get_typtype(ret->typid) == TYPTYPE_PSEUDO)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("returning pseudo-types is not supported in SQL/JSON functions"));
+
if (ret->format->format_type == JS_FORMAT_DEFAULT)
/* assign JSONB format when returning jsonb, or JSON format otherwise */
ret->format->format_type =
@@ -3621,7 +3686,7 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor)
Node *val = transformJsonValueExpr(pstate, "JSON_OBJECT()",
kv->value,
JS_FORMAT_DEFAULT,
- InvalidOid);
+ InvalidOid, false);
args = lappend(args, key);
args = lappend(args, val);
@@ -3808,7 +3873,7 @@ transformJsonObjectAgg(ParseState *pstate, JsonObjectAgg *agg)
val = transformJsonValueExpr(pstate, "JSON_OBJECTAGG()",
agg->arg->value,
JS_FORMAT_DEFAULT,
- InvalidOid);
+ InvalidOid, false);
args = list_make2(key, val);
returning = transformJsonConstructorOutput(pstate, agg->constructor->output,
@@ -3864,9 +3929,8 @@ transformJsonArrayAgg(ParseState *pstate, JsonArrayAgg *agg)
Oid aggfnoid;
Oid aggtype;
- arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()",
- agg->arg,
- JS_FORMAT_DEFAULT, InvalidOid);
+ arg = transformJsonValueExpr(pstate, "JSON_ARRAYAGG()", agg->arg,
+ JS_FORMAT_DEFAULT, InvalidOid, false);
returning = transformJsonConstructorOutput(pstate, agg->constructor->output,
list_make1(arg));
@@ -3913,9 +3977,8 @@ transformJsonArrayConstructor(ParseState *pstate, JsonArrayConstructor *ctor)
{
JsonValueExpr *jsval = castNode(JsonValueExpr, lfirst(lc));
Node *val = transformJsonValueExpr(pstate, "JSON_ARRAY()",
- jsval,
- JS_FORMAT_DEFAULT,
- InvalidOid);
+ jsval, JS_FORMAT_DEFAULT,
+ InvalidOid, false);
args = lappend(args, val);
}
@@ -4074,7 +4137,7 @@ transformJsonParseExpr(ParseState *pstate, JsonParseExpr *jsexpr)
* function-like CASTs.
*/
arg = transformJsonValueExpr(pstate, "JSON()", jsexpr->expr,
- JS_FORMAT_JSON, returning->typid);
+ JS_FORMAT_JSON, returning->typid, false);
}
return makeJsonConstructorExpr(pstate, JSCTOR_JSON_PARSE, list_make1(arg), NULL,
@@ -4119,7 +4182,7 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr)
Node *arg = transformJsonValueExpr(pstate, "JSON_SERIALIZE()",
expr->expr,
JS_FORMAT_JSON,
- InvalidOid);
+ InvalidOid, false);
if (expr->output)
{
@@ -4153,3 +4216,565 @@ transformJsonSerializeExpr(ParseState *pstate, JsonSerializeExpr *expr)
return makeJsonConstructorExpr(pstate, JSCTOR_JSON_SERIALIZE, list_make1(arg),
NULL, returning, false, false, expr->location);
}
+
+/*
+ * Transform JSON_VALUE, JSON_QUERY, JSON_EXISTS functions into a JsonExpr node.
+ */
+static Node *
+transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
+{
+ JsonExpr *jsexpr = NULL;
+ const char *func_name = NULL;
+ JsonBehavior *on_error = NULL,
+ *on_empty = NULL;
+
+ /*
+ * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS()
+ * and JSON_VALUE().
+ */
+ if (func->output &&
+ (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP))
+ {
+ JsonFormat *format = func->output->returning->format;
+
+ if (format->format_type != JS_FORMAT_DEFAULT ||
+ format->encoding != JS_ENC_DEFAULT)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot specify FORMAT in RETURNING clause of %s",
+ func->op == JSON_VALUE_OP ? "JSON_VALUE()" :
+ "JSON_EXISTS()"),
+ parser_errposition(pstate, format->location)));
+ }
+
+ if (func->behavior)
+ {
+ on_empty = linitial(func->behavior);
+ on_error = lsecond(func->behavior);
+ }
+
+ switch (func->op)
+ {
+ case JSON_EXISTS_OP:
+ func_name = "JSON_EXISTS";
+
+ jsexpr = transformJsonExprCommon(pstate, func, func_name);
+
+ if (!OidIsValid(jsexpr->returning->typid))
+ {
+ jsexpr->returning->typid = BOOLOID;
+ jsexpr->returning->typmod = -1;
+ }
+ else if (jsexpr->returning->typid != BOOLOID)
+ {
+ Node *coercion_expr;
+ CaseTestExpr *placeholder = makeNode(CaseTestExpr);
+ int location = exprLocation((Node *) jsexpr);
+
+ /*
+ * We abuse CaseTestExpr here as placeholder to pass the
+ * result of evaluating JSON_EXISTS to the coercion
+ * expression.
+ */
+ placeholder->typeId = BOOLOID;
+ placeholder->typeMod = -1;
+ placeholder->collation = InvalidOid;
+
+ coercion_expr =
+ coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID,
+ jsexpr->returning->typid,
+ jsexpr->returning->typmod,
+ COERCION_EXPLICIT,
+ COERCE_IMPLICIT_CAST,
+ location);
+
+ if (coercion_expr == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot cast type %s to %s",
+ format_type_be(BOOLOID),
+ format_type_be(jsexpr->returning->typid)),
+ parser_coercion_errposition(pstate, location, (Node *) jsexpr)));
+
+ if (coercion_expr != (Node *) placeholder)
+ jsexpr->result_coercion = coercion_expr;
+ }
+
+ jsexpr->on_error = transformJsonBehavior(pstate, on_error,
+ JSON_BEHAVIOR_FALSE,
+ jsexpr->returning);
+ break;
+
+ case JSON_QUERY_OP:
+ func_name = "JSON_QUERY";
+
+ if (func->wrapper != JSW_NONE && func->quotes != JS_QUOTES_UNSPEC)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"),
+ parser_errposition(pstate, func->location)));
+
+ jsexpr = transformJsonExprCommon(pstate, func, func_name);
+ jsexpr->wrapper = func->wrapper;
+ jsexpr->omit_quotes = (func->quotes == JS_QUOTES_OMIT);
+
+ if (!OidIsValid(jsexpr->returning->typid))
+ {
+ JsonReturning *ret = jsexpr->returning;
+
+ ret->typid = JsonFuncExprDefaultReturnType(jsexpr);
+ ret->typmod = -1;
+ }
+ jsexpr->result_coercion = coerceJsonFuncExprOutput(pstate, jsexpr);
+
+ if (on_empty)
+ jsexpr->on_empty = transformJsonBehavior(pstate, on_empty,
+ JSON_BEHAVIOR_NULL,
+ jsexpr->returning);
+ jsexpr->on_error = transformJsonBehavior(pstate, on_error,
+ JSON_BEHAVIOR_NULL,
+ jsexpr->returning);
+ break;
+
+ case JSON_VALUE_OP:
+ func_name = "JSON_VALUE";
+
+ jsexpr = transformJsonExprCommon(pstate, func, func_name);
+
+ if (!OidIsValid(jsexpr->returning->typid))
+ {
+ /* Make JSON_VALUE return text by default */
+ jsexpr->returning->typid = TEXTOID;
+ jsexpr->returning->typmod = -1;
+ }
+ jsexpr->result_coercion = coerceJsonFuncExprOutput(pstate, jsexpr);
+
+ /*
+ * Initialize expressions to coerce the scalar value returned
+ * by JsonPathValue() to the "returning" type.
+ */
+ if (jsexpr->result_coercion)
+ jsexpr->item_coercions =
+ InitJsonItemCoercions(pstate, jsexpr->returning,
+ exprType(jsexpr->formatted_expr));
+
+ if (on_empty)
+ jsexpr->on_empty = transformJsonBehavior(pstate, on_empty,
+ JSON_BEHAVIOR_NULL,
+ jsexpr->returning);
+ jsexpr->on_error = transformJsonBehavior(pstate, on_error,
+ JSON_BEHAVIOR_NULL,
+ jsexpr->returning);
+ break;
+ }
+
+ return (Node *) jsexpr;
+}
+
+/*
+ * Common code for JSON_VALUE, JSON_QUERY, JSON_EXISTS transformation
+ * into a JsonExpr node.
+ */
+static JsonExpr *
+transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func,
+ const char *constructName)
+{
+ JsonExpr *jsexpr = makeNode(JsonExpr);
+ Node *pathspec;
+
+ jsexpr->location = func->location;
+ jsexpr->op = func->op;
+ jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName,
+ func->context_item,
+ JS_FORMAT_JSON,
+ InvalidOid, false);
+
+ Assert(jsexpr->formatted_expr != NULL);
+ if (exprType(jsexpr->formatted_expr) != JSONBOID)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("%s() is not yet implemented for the json type",
+ constructName),
+ errhint("Try casting the argument to jsonb"),
+ parser_errposition(pstate, exprLocation(jsexpr->formatted_expr))));
+
+ jsexpr->format = func->context_item->format;
+
+ /* Both set in the caller. */
+ jsexpr->result_coercion = NULL;
+ jsexpr->omit_quotes = false;
+
+ pathspec = transformExprRecurse(pstate, func->pathspec);
+
+ jsexpr->path_spec =
+ coerce_to_target_type(pstate, pathspec, exprType(pathspec),
+ JSONPATHOID, -1,
+ COERCION_EXPLICIT, COERCE_IMPLICIT_CAST,
+ exprLocation(pathspec));
+ if (!jsexpr->path_spec)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("JSON path expression must be of type %s, not of type %s",
+ "jsonpath", format_type_be(exprType(pathspec))),
+ parser_errposition(pstate, exprLocation(pathspec))));
+
+ /*
+ * Transform and coerce to json[b] passing arguments, whose format is
+ * determined by context item type.
+ */
+ transformJsonPassingArgs(pstate, constructName,
+ exprType(jsexpr->formatted_expr) == JSONBOID ?
+ JS_FORMAT_JSONB : JS_FORMAT_JSON,
+ func->passing,
+ &jsexpr->passing_values,
+ &jsexpr->passing_names);
+
+ jsexpr->returning = transformJsonOutput(pstate, func->output, false);
+
+ /* JSON_QUERY supports specifying FORMAT explicitly. */
+ if (func->op != JSON_QUERY_OP)
+ {
+ jsexpr->returning->format->format_type = JS_FORMAT_DEFAULT;
+ jsexpr->returning->format->encoding = JS_ENC_DEFAULT;
+ }
+
+ return jsexpr;
+}
+
+/*
+ * Transform a JSON PASSING clause.
+ */
+static void
+transformJsonPassingArgs(ParseState *pstate, const char *constructName,
+ JsonFormatType format, List *args,
+ List **passing_values, List **passing_names)
+{
+ ListCell *lc;
+
+ *passing_values = NIL;
+ *passing_names = NIL;
+
+ foreach(lc, args)
+ {
+ JsonArgument *arg = castNode(JsonArgument, lfirst(lc));
+ Node *expr = transformJsonValueExpr(pstate, constructName,
+ arg->val, format,
+ InvalidOid, true);
+
+ *passing_values = lappend(*passing_values, expr);
+ *passing_names = lappend(*passing_names, makeString(arg->name));
+ }
+}
+
+/*
+ * Transform the JSON output clause of JSON_VALUE and JSON_QUERY.
+ */
+static Node *
+coerceJsonFuncExprOutput(ParseState *pstate, JsonExpr *jsexpr)
+{
+ JsonReturning *returning = jsexpr->returning;
+ Node *context_item = jsexpr->formatted_expr;
+ int default_typmod;
+ Oid default_typid;
+
+ Assert(returning);
+
+ /*
+ * For JSON_QUERY, use forced coercion via I/O to implement the specified
+ * QUOTES or WRAPPER behavior.
+ */
+ if (jsexpr->op == JSON_QUERY_OP &&
+ (jsexpr->omit_quotes || jsexpr->wrapper != JSW_NONE))
+ {
+ JsonCoercion *coercion = makeNode(JsonCoercion);
+
+ coercion->targettype = returning->typid;
+ coercion->targettypmod = returning->typmod;
+ coercion->via_io = jsexpr->omit_quotes;
+ coercion->via_populate = jsexpr->wrapper != JSW_NONE;
+
+ return (Node *) coercion;
+ }
+
+ default_typid = JsonFuncExprDefaultReturnType(jsexpr);
+ default_typmod = -1;
+ if (returning->typid != default_typid ||
+ returning->typmod != default_typmod)
+ {
+ /*
+ * We abuse CaseTestExpr here as placeholder to pass the result of
+ * evaluating the JSON_VALUE/QUERY jsonpath expression to the coercion
+ * function.
+ */
+ CaseTestExpr *placeholder = makeNode(CaseTestExpr);
+
+ placeholder->typeId = exprType(context_item);
+ placeholder->typeMod = exprTypmod(context_item);
+ placeholder->collation = exprCollation(context_item);
+
+ Assert(placeholder->typeId == default_typid);
+ Assert(placeholder->typeMod == default_typmod);
+
+ return coerceJsonExpr(pstate, (Node *) placeholder, returning);
+ }
+
+ return NULL;
+}
+
+/* Returns the default type for a given JsonExpr for a given JsonFormat. */
+static Oid
+JsonFuncExprDefaultReturnType(JsonExpr *jsexpr)
+{
+ JsonFormat *format = jsexpr->format;
+ Node *context_item = jsexpr->formatted_expr;
+
+ Assert(format);
+ if (format->format_type == JS_FORMAT_JSONB)
+ return JSONBOID;
+ else if (format->format_type == JS_FORMAT_DEFAULT &&
+ exprType(context_item) == JSONBOID)
+ return JSONBOID;
+
+ return JSONOID;
+}
+
+/*
+ * Returns a JsonCoercion node to coerce a jsonb-valued expression to the
+ * target type given by 'returning' using either json_populate_type() or
+ * by using the target type's input function.
+ */
+static JsonCoercion *
+makeJsonCoercion(const JsonReturning *returning)
+{
+ JsonCoercion *coercion = makeNode(JsonCoercion);
+ char typtype;
+
+ coercion->targettype = returning->typid;
+ coercion->targettypmod = returning->typmod;
+
+ typtype = get_typtype(returning->typid);
+ if (returning->typid == RECORDOID ||
+ typtype == TYPTYPE_COMPOSITE ||
+ typtype == TYPTYPE_DOMAIN ||
+ type_is_array(returning->typid))
+ coercion->via_populate = true;
+ else
+ coercion->via_io = true;
+
+ return coercion;
+}
+
+/*
+ * Set up a JsonCoercion node to:
+ *
+ * - coerce expression to the output returning type, or
+ * - coerce using json_populate_type() if returning type requires it, or
+ * - coerce via I/O.
+ */
+static Node *
+coerceJsonExpr(ParseState *pstate, Node *expr, const JsonReturning *returning)
+{
+ Node *coerced_expr;
+
+ coerced_expr = coerceJsonFuncExpr(pstate, expr, returning, false);
+ if (coerced_expr)
+ {
+ if (coerced_expr == expr)
+ return NULL;
+ return coerced_expr;
+ }
+
+ return (Node *) makeJsonCoercion(returning);
+}
+
+/*
+ * Initialize JsonCoercion nodes for coercing a given JSON item value produced
+ * by JSON_VALUE to the target "returning" type; also see
+ * ExecPrepareJsonItemCoercion().
+ */
+static List *
+InitJsonItemCoercions(ParseState *pstate, const JsonReturning *returning,
+ Oid contextItemTypeId)
+{
+ List *item_coercions = NIL;
+ int i;
+ Oid typeoid;
+ struct
+ {
+ JsonItemType item_type;
+ Oid typeoid;
+ } item_types[] =
+ {
+ {JsonItemTypeNull, UNKNOWNOID},
+ {JsonItemTypeString, TEXTOID},
+ {JsonItemTypeNumeric, NUMERICOID},
+ {JsonItemTypeBoolean, BOOLOID},
+ {JsonItemTypeDate, DATEOID},
+ {JsonItemTypeTime, TIMEOID},
+ {JsonItemTypeTimetz, TIMETZOID},
+ {JsonItemTypeTimestamp, TIMESTAMPOID},
+ {JsonItemTypeTimestamptz, TIMESTAMPTZOID},
+ {JsonItemTypeComposite, contextItemTypeId},
+ {JsonItemTypeInvalid, InvalidOid}
+ };
+
+ for (i = 0; OidIsValid(typeoid = item_types[i].typeoid); i++)
+ {
+ Node *expr;
+ JsonItemCoercion *item_coercion = makeNode(JsonItemCoercion);
+
+ if (typeoid == UNKNOWNOID)
+ {
+ expr = (Node *) makeNullConst(UNKNOWNOID, -1, InvalidOid);
+ }
+ else
+ {
+ CaseTestExpr *placeholder = makeNode(CaseTestExpr);
+
+ /*
+ * We abuse CaseTestExpr here as placeholder to pass the result of
+ * JSON_VALUE jsonpath expression to the coercion function.
+ */
+ placeholder->typeId = item_types[i].typeoid;
+ placeholder->typeMod = -1;
+ placeholder->collation = InvalidOid;
+
+ expr = (Node *) placeholder;
+ }
+
+ item_coercion->item_type = item_types[i].item_type;
+ item_coercion->coercion = coerceJsonExpr(pstate, expr, returning);
+ item_coercions = lappend(item_coercions, item_coercion);
+ }
+
+ return item_coercions;
+}
+
+/*
+ * Returns constant values to be returned to the user for various
+ * non-ERROR ON ERROR/EMPTY behaviors.
+ *
+ * Note that JSON_BEHAVIOR_DEFAULT expression is handled by the
+ * caller separately.
+ */
+static Node *
+GetJsonBehaviorConstExpr(JsonBehaviorType btype, int location)
+{
+ Datum val = (Datum) 0;
+ Oid typid = JSONBOID;
+ int len = -1;
+ bool isbyval = false;
+ bool isnull = false;
+ Const *con;
+
+ switch (btype)
+ {
+ case JSON_BEHAVIOR_EMPTY_ARRAY:
+ val = DirectFunctionCall1(jsonb_in, CStringGetDatum("[]"));
+ break;
+
+ case JSON_BEHAVIOR_EMPTY_OBJECT:
+ val = DirectFunctionCall1(jsonb_in, CStringGetDatum("{}"));
+ break;
+
+ case JSON_BEHAVIOR_TRUE:
+ val = BoolGetDatum(true);
+ typid = BOOLOID;
+ len = sizeof(bool);
+ isbyval = true;
+ break;
+
+ case JSON_BEHAVIOR_FALSE:
+ val = BoolGetDatum(false);
+ typid = BOOLOID;
+ len = sizeof(bool);
+ isbyval = true;
+ break;
+
+ case JSON_BEHAVIOR_NULL:
+ case JSON_BEHAVIOR_UNKNOWN:
+ case JSON_BEHAVIOR_EMPTY:
+ val = (Datum) 0;
+ isnull = true;
+ typid = INT4OID;
+ len = sizeof(int32);
+ isbyval = true;
+ break;
+
+ case JSON_BEHAVIOR_DEFAULT:
+ case JSON_BEHAVIOR_ERROR:
+ /* Always handled in the caller. */
+ Assert(false);
+ break;
+
+ default:
+ elog(ERROR, "unrecognized SQL/JSON behavior %d", btype);
+ break;
+ }
+
+ con = makeConst(typid, -1, InvalidOid, len, val, isnull, isbyval);
+ con->location = location;
+
+ return (Node *) con;
+}
+
+/*
+ * Transform a JSON BEHAVIOR clause.
+ */
+static JsonBehavior *
+transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
+ JsonBehaviorType default_behavior,
+ JsonReturning *returning)
+{
+ JsonBehaviorType behavior_type = default_behavior;
+ Node *expr = NULL;
+ JsonCoercion *coercion = NULL;
+ int location = -1;
+
+ if (behavior)
+ {
+ behavior_type = behavior->btype;
+ location = behavior->location;
+ if (behavior_type == JSON_BEHAVIOR_DEFAULT)
+ expr = transformExprRecurse(pstate, behavior->expr);
+ }
+
+ if (expr == NULL && behavior_type != JSON_BEHAVIOR_ERROR)
+ expr = GetJsonBehaviorConstExpr(behavior_type, location);
+
+ if (expr)
+ {
+ Node *coerced_expr = expr;
+ bool isnull = (IsA(expr, Const) && ((Const *) expr)->constisnull);
+
+ /*
+ * Coerce NULLs and "default" (that is, not specified by the user)
+ * jsonb-valued expressions using a JsonCoercion node.
+ *
+ * For other (user-specified) non-NULL values, try to find a cast
+ * and error out if one is not found.
+ */
+ if (isnull ||
+ (exprType(expr) == JSONBOID &&
+ behavior_type == default_behavior))
+ coercion = makeJsonCoercion(returning);
+ else
+ coerced_expr =
+ coerce_to_target_type(pstate, expr, exprType(expr),
+ returning->typid, returning->typmod,
+ COERCION_EXPLICIT, COERCE_EXPLICIT_CAST,
+ exprLocation((Node *) behavior));
+
+ if (coerced_expr == NULL)
+ ereport(ERROR,
+ errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot cast behavior expression of type %s to %s",
+ format_type_be(exprType(expr)),
+ format_type_be(returning->typid)),
+ parser_errposition(pstate, exprLocation(expr)));
+ else
+ expr = coerced_expr;
+ }
+
+ return makeJsonBehavior(behavior_type, expr, coercion, location);
+}
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 3bc62ac3ba..8f81624f97 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1989,6 +1989,21 @@ FigureColnameInternal(Node *node, char **name)
/* make JSON_ARRAYAGG act like a regular function */
*name = "json_arrayagg";
return 2;
+ case T_JsonFuncExpr:
+ /* make SQL/JSON functions act like a regular function */
+ switch (((JsonFuncExpr *) node)->op)
+ {
+ case JSON_EXISTS_OP:
+ *name = "json_exists";
+ return 2;
+ case JSON_QUERY_OP:
+ *name = "json_query";
+ return 2;
+ case JSON_VALUE_OP:
+ *name = "json_value";
+ return 2;
+ }
+ break;
default:
break;
}
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index d176723d95..5d3c01f41f 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -4426,6 +4426,50 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
}
}
+/*
+ * Parses the datetime format string in 'fmt_str' and returns true if it
+ * contains a timezone specifier, false if not.
+ */
+bool
+datetime_format_has_tz(const char *fmt_str)
+{
+ bool incache;
+ int fmt_len = strlen(fmt_str);
+ int result;
+ FormatNode *format;
+
+ if (fmt_len > DCH_CACHE_SIZE)
+ {
+ /*
+ * Allocate new memory if format picture is bigger than static cache
+ * and do not use cache (call parser always)
+ */
+ incache = false;
+
+ format = (FormatNode *) palloc((fmt_len + 1) * sizeof(FormatNode));
+
+ parse_format(format, fmt_str, DCH_keywords,
+ DCH_suff, DCH_index, DCH_FLAG, NULL);
+ }
+ else
+ {
+ /*
+ * Use cache buffers
+ */
+ DCHCacheEntry *ent = DCH_cache_fetch(fmt_str, false);
+
+ incache = true;
+ format = ent->format;
+ }
+
+ result = DCH_datetime_type(format);
+
+ if (!incache)
+ pfree(format);
+
+ return result & DCH_ZONED;
+}
+
/*
* do_to_timestamp: shared code for to_timestamp and to_date
*
diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c
index 6f445f5c2b..e5dca46b96 100644
--- a/src/backend/utils/adt/jsonb.c
+++ b/src/backend/utils/adt/jsonb.c
@@ -2163,3 +2163,34 @@ jsonb_float8(PG_FUNCTION_ARGS)
PG_RETURN_DATUM(retValue);
}
+
+/*
+ * Convert jsonb to a C-string stripping quotes from scalar strings.
+ */
+char *
+JsonbUnquote(Jsonb *jb)
+{
+ if (JB_ROOT_IS_SCALAR(jb))
+ {
+ JsonbValue v;
+
+ (void) JsonbExtractScalar(&jb->root, &v);
+
+ if (v.type == jbvString)
+ return pnstrdup(v.val.string.val, v.val.string.len);
+ else if (v.type == jbvBool)
+ return pstrdup(v.val.boolean ? "true" : "false");
+ else if (v.type == jbvNumeric)
+ return DatumGetCString(DirectFunctionCall1(numeric_out,
+ PointerGetDatum(v.val.numeric)));
+ else if (v.type == jbvNull)
+ return pstrdup("null");
+ else
+ {
+ elog(ERROR, "unrecognized jsonb value type %d", v.type);
+ return NULL;
+ }
+ }
+ else
+ return JsonbToCString(NULL, &jb->root, VARSIZE(jb));
+}
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index 1574ed5985..b495aedce1 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -2805,7 +2805,8 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
check_stack_depth();
- if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc))
+ if (jbv->type != jbvBinary || !JsonContainerIsArray(jbc) ||
+ JsonContainerIsScalar(jbc))
{
populate_array_report_expected_array(ctx, ndim - 1);
/* Getting here means the error was reported softly. */
@@ -2813,8 +2814,6 @@ populate_array_dim_jsonb(PopulateArrayContext *ctx, /* context */
return false;
}
- Assert(!JsonContainerIsScalar(jbc));
-
it = JsonbIteratorInit(jbc);
tok = JsonbIteratorNext(&it, &val, true);
@@ -3349,6 +3348,53 @@ populate_record_field(ColumnIOData *col,
}
}
+/* recursively populate specified type from a json/jsonb value */
+Datum
+json_populate_type(Datum json_val, Oid json_type, Oid typid, int32 typmod,
+ void **cache, MemoryContext mcxt, bool *isnull,
+ Node *escontext)
+{
+ JsValue jsv = {0};
+ JsonbValue jbv;
+
+ jsv.is_json = json_type == JSONOID;
+
+ if (*isnull)
+ {
+ if (jsv.is_json)
+ jsv.val.json.str = NULL;
+ else
+ jsv.val.jsonb = NULL;
+ }
+ else if (jsv.is_json)
+ {
+ text *json = DatumGetTextPP(json_val);
+
+ jsv.val.json.str = VARDATA_ANY(json);
+ jsv.val.json.len = VARSIZE_ANY_EXHDR(json);
+ jsv.val.json.type = JSON_TOKEN_INVALID; /* not used in
+ * populate_composite() */
+ }
+ else
+ {
+ Jsonb *jsonb = DatumGetJsonbP(json_val);
+
+ jsv.val.jsonb = &jbv;
+
+ /* fill binary jsonb value pointing to jb */
+ jbv.type = jbvBinary;
+ jbv.val.binary.data = &jsonb->root;
+ jbv.val.binary.len = VARSIZE(jsonb) - VARHDRSZ;
+ }
+
+ if (!*cache)
+ *cache = MemoryContextAllocZero(mcxt, sizeof(ColumnIOData));
+
+ return populate_record_field(*cache, typid, typmod, NULL, mcxt,
+ PointerGetDatum(NULL), &jsv, isnull,
+ escontext);
+}
+
static RecordIOData *
allocate_record_info(MemoryContext mcxt, int ncolumns)
{
diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c
index c5ba3b7f1d..9d53e4a992 100644
--- a/src/backend/utils/adt/jsonpath.c
+++ b/src/backend/utils/adt/jsonpath.c
@@ -68,7 +68,9 @@
#include "libpq/pqformat.h"
#include "nodes/miscnodes.h"
#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
#include "utils/builtins.h"
+#include "utils/formatting.h"
#include "utils/json.h"
#include "utils/jsonpath.h"
@@ -1110,3 +1112,256 @@ jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from, JsonPathItem *to,
return true;
}
+
+/* SQL/JSON datatype status: */
+typedef enum JsonPathDatatypeStatus
+{
+ jpdsNonDateTime, /* null, bool, numeric, string, array, object */
+ jpdsUnknownDateTime, /* unknown datetime type */
+ jpdsDateTimeZoned, /* timetz, timestamptz */
+ jpdsDateTimeNonZoned, /* time, timestamp, date */
+} JsonPathDatatypeStatus;
+
+/* Context for jspIsMutableWalker() */
+typedef struct JsonPathMutableContext
+{
+ List *varnames; /* list of variable names */
+ List *varexprs; /* list of variable expressions */
+ JsonPathDatatypeStatus current; /* status of @ item */
+ bool lax; /* jsonpath is lax or strict */
+ bool mutable; /* resulting mutability status */
+} JsonPathMutableContext;
+
+/*
+ * Recursive walker for jspIsMutable()
+ */
+static JsonPathDatatypeStatus
+jspIsMutableWalker(JsonPathItem *jpi, JsonPathMutableContext *cxt)
+{
+ JsonPathItem next;
+ JsonPathDatatypeStatus status = jpdsNonDateTime;
+
+ while (!cxt->mutable)
+ {
+ JsonPathItem arg;
+ JsonPathDatatypeStatus leftStatus;
+ JsonPathDatatypeStatus rightStatus;
+
+ switch (jpi->type)
+ {
+ case jpiRoot:
+ Assert(status == jpdsNonDateTime);
+ break;
+
+ case jpiCurrent:
+ Assert(status == jpdsNonDateTime);
+ status = cxt->current;
+ break;
+
+ case jpiFilter:
+ {
+ JsonPathDatatypeStatus prevStatus = cxt->current;
+
+ cxt->current = status;
+ jspGetArg(jpi, &arg);
+ jspIsMutableWalker(&arg, cxt);
+
+ cxt->current = prevStatus;
+ break;
+ }
+
+ case jpiVariable:
+ {
+ int32 len;
+ const char *name = jspGetString(jpi, &len);
+ ListCell *lc1;
+ ListCell *lc2;
+
+ Assert(status == jpdsNonDateTime);
+
+ forboth(lc1, cxt->varnames, lc2, cxt->varexprs)
+ {
+ String *varname = lfirst_node(String, lc1);
+ Node *varexpr = lfirst(lc2);
+
+ if (strncmp(varname->sval, name, len))
+ continue;
+
+ switch (exprType(varexpr))
+ {
+ case DATEOID:
+ case TIMEOID:
+ case TIMESTAMPOID:
+ status = jpdsDateTimeNonZoned;
+ break;
+
+ case TIMETZOID:
+ case TIMESTAMPTZOID:
+ status = jpdsDateTimeZoned;
+ break;
+
+ default:
+ status = jpdsNonDateTime;
+ break;
+ }
+
+ break;
+ }
+ break;
+ }
+
+ case jpiEqual:
+ case jpiNotEqual:
+ case jpiLess:
+ case jpiGreater:
+ case jpiLessOrEqual:
+ case jpiGreaterOrEqual:
+ Assert(status == jpdsNonDateTime);
+ jspGetLeftArg(jpi, &arg);
+ leftStatus = jspIsMutableWalker(&arg, cxt);
+
+ jspGetRightArg(jpi, &arg);
+ rightStatus = jspIsMutableWalker(&arg, cxt);
+
+ /*
+ * Comparison of datetime type with different timezone status
+ * is mutable.
+ */
+ if (leftStatus != jpdsNonDateTime &&
+ rightStatus != jpdsNonDateTime &&
+ (leftStatus == jpdsUnknownDateTime ||
+ rightStatus == jpdsUnknownDateTime ||
+ leftStatus != rightStatus))
+ cxt->mutable = true;
+ break;
+
+ case jpiNot:
+ case jpiIsUnknown:
+ case jpiExists:
+ case jpiPlus:
+ case jpiMinus:
+ Assert(status == jpdsNonDateTime);
+ jspGetArg(jpi, &arg);
+ jspIsMutableWalker(&arg, cxt);
+ break;
+
+ case jpiAnd:
+ case jpiOr:
+ case jpiAdd:
+ case jpiSub:
+ case jpiMul:
+ case jpiDiv:
+ case jpiMod:
+ case jpiStartsWith:
+ Assert(status == jpdsNonDateTime);
+ jspGetLeftArg(jpi, &arg);
+ jspIsMutableWalker(&arg, cxt);
+ jspGetRightArg(jpi, &arg);
+ jspIsMutableWalker(&arg, cxt);
+ break;
+
+ case jpiIndexArray:
+ for (int i = 0; i < jpi->content.array.nelems; i++)
+ {
+ JsonPathItem from;
+ JsonPathItem to;
+
+ if (jspGetArraySubscript(jpi, &from, &to, i))
+ jspIsMutableWalker(&to, cxt);
+
+ jspIsMutableWalker(&from, cxt);
+ }
+ /* FALLTHROUGH */
+
+ case jpiAnyArray:
+ if (!cxt->lax)
+ status = jpdsNonDateTime;
+ break;
+
+ case jpiAny:
+ if (jpi->content.anybounds.first > 0)
+ status = jpdsNonDateTime;
+ break;
+
+ case jpiDatetime:
+ if (jpi->content.arg)
+ {
+ char *template;
+
+ jspGetArg(jpi, &arg);
+ if (arg.type != jpiString)
+ {
+ status = jpdsNonDateTime;
+ break; /* there will be runtime error */
+ }
+
+ template = jspGetString(&arg, NULL);
+ if (datetime_format_has_tz(template))
+ status = jpdsDateTimeZoned;
+ else
+ status = jpdsDateTimeNonZoned;
+ }
+ else
+ {
+ status = jpdsUnknownDateTime;
+ }
+ break;
+
+ case jpiLikeRegex:
+ Assert(status == jpdsNonDateTime);
+ jspInitByBuffer(&arg, jpi->base, jpi->content.like_regex.expr);
+ jspIsMutableWalker(&arg, cxt);
+ break;
+
+ /* literals */
+ case jpiNull:
+ case jpiString:
+ case jpiNumeric:
+ case jpiBool:
+ /* accessors */
+ case jpiKey:
+ case jpiAnyKey:
+ /* special items */
+ case jpiSubscript:
+ case jpiLast:
+ /* item methods */
+ case jpiType:
+ case jpiSize:
+ case jpiAbs:
+ case jpiFloor:
+ case jpiCeiling:
+ case jpiDouble:
+ case jpiKeyValue:
+ status = jpdsNonDateTime;
+ break;
+ }
+
+ if (!jspGetNext(jpi, &next))
+ break;
+
+ jpi = &next;
+ }
+
+ return status;
+}
+
+/*
+ * Check whether jsonpath expression is immutable or not.
+ */
+bool
+jspIsMutable(JsonPath *path, List *varnames, List *varexprs)
+{
+ JsonPathMutableContext cxt;
+ JsonPathItem jpi;
+
+ cxt.varnames = varnames;
+ cxt.varexprs = varexprs;
+ cxt.current = jpdsNonDateTime;
+ cxt.lax = (path->header & JSONPATH_LAX) != 0;
+ cxt.mutable = false;
+
+ jspInit(&jpi, path);
+ jspIsMutableWalker(&jpi, &cxt);
+
+ return cxt.mutable;
+}
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 2d0599b4aa..6c5602c64d 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -87,12 +87,16 @@ typedef struct JsonBaseObjectInfo
int id;
} JsonBaseObjectInfo;
+typedef int (*JsonPathVarCallback) (void *vars, char *varName, int varNameLen,
+ JsonbValue *val, JsonbValue *baseObject);
+
/*
* Context of jsonpath execution.
*/
typedef struct JsonPathExecContext
{
- Jsonb *vars; /* variables to substitute into jsonpath */
+ void *vars; /* variables to substitute into jsonpath */
+ JsonPathVarCallback getVar;
JsonbValue *root; /* for $ evaluation */
JsonbValue *current; /* for @ evaluation */
JsonBaseObjectInfo baseObject; /* "base object" for .keyvalue()
@@ -174,7 +178,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp,
void *param);
typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error);
-static JsonPathExecResult executeJsonPath(JsonPath *path, Jsonb *vars,
+static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars,
+ JsonPathVarCallback getVar,
Jsonb *json, bool throwErrors,
JsonValueList *result, bool useTz);
static JsonPathExecResult executeItem(JsonPathExecContext *cxt,
@@ -225,8 +230,13 @@ static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt,
JsonPathItem *jsp, JsonValueList *found, JsonPathBool res);
static void getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item,
JsonbValue *value);
+static int GetJsonPathVar(void *vars, char *varName, int varNameLen,
+ JsonbValue *val, JsonbValue *baseObject);
static void getJsonPathVariable(JsonPathExecContext *cxt,
- JsonPathItem *variable, Jsonb *vars, JsonbValue *value);
+ JsonPathItem *variable, JsonbValue *value);
+static int getJsonPathVariableFromJsonb(void *varsJsonb, char *varName,
+ int varNameLen, JsonbValue *val,
+ JsonbValue *baseObject);
static int JsonbArraySize(JsonbValue *jb);
static JsonPathBool executeComparison(JsonPathItem *cmp, JsonbValue *lv,
JsonbValue *rv, void *p);
@@ -284,7 +294,8 @@ jsonb_path_exists_internal(FunctionCallInfo fcinfo, bool tz)
silent = PG_GETARG_BOOL(3);
}
- res = executeJsonPath(jp, vars, jb, !silent, NULL, tz);
+ res = executeJsonPath(jp, vars, getJsonPathVariableFromJsonb,
+ jb, !silent, NULL, tz);
PG_FREE_IF_COPY(jb, 0);
PG_FREE_IF_COPY(jp, 1);
@@ -339,7 +350,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz)
silent = PG_GETARG_BOOL(3);
}
- (void) executeJsonPath(jp, vars, jb, !silent, &found, tz);
+ (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb,
+ jb, !silent, &found, tz);
PG_FREE_IF_COPY(jb, 0);
PG_FREE_IF_COPY(jp, 1);
@@ -417,7 +429,8 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz)
vars = PG_GETARG_JSONB_P_COPY(2);
silent = PG_GETARG_BOOL(3);
- (void) executeJsonPath(jp, vars, jb, !silent, &found, tz);
+ (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb,
+ jb, !silent, &found, tz);
funcctx->user_fctx = JsonValueListGetList(&found);
@@ -464,7 +477,8 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz)
Jsonb *vars = PG_GETARG_JSONB_P(2);
bool silent = PG_GETARG_BOOL(3);
- (void) executeJsonPath(jp, vars, jb, !silent, &found, tz);
+ (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb,
+ jb, !silent, &found, tz);
PG_RETURN_JSONB_P(JsonbValueToJsonb(wrapItemsInArray(&found)));
}
@@ -495,7 +509,8 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz)
Jsonb *vars = PG_GETARG_JSONB_P(2);
bool silent = PG_GETARG_BOOL(3);
- (void) executeJsonPath(jp, vars, jb, !silent, &found, tz);
+ (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb,
+ jb, !silent, &found, tz);
if (JsonValueListLength(&found) >= 1)
PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found)));
@@ -537,8 +552,9 @@ jsonb_path_query_first_tz(PG_FUNCTION_ARGS)
* In other case it tries to find all the satisfied result items.
*/
static JsonPathExecResult
-executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors,
- JsonValueList *result, bool useTz)
+executeJsonPath(JsonPath *path, void *vars, JsonPathVarCallback getVar,
+ Jsonb *json, bool throwErrors, JsonValueList *result,
+ bool useTz)
{
JsonPathExecContext cxt;
JsonPathExecResult res;
@@ -550,22 +566,16 @@ executeJsonPath(JsonPath *path, Jsonb *vars, Jsonb *json, bool throwErrors,
if (!JsonbExtractScalar(&json->root, &jbv))
JsonbInitBinary(&jbv, json);
- if (vars && !JsonContainerIsObject(&vars->root))
- {
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("\"vars\" argument is not an object"),
- errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object.")));
- }
-
cxt.vars = vars;
+ cxt.getVar = getVar;
cxt.laxMode = (path->header & JSONPATH_LAX) != 0;
cxt.ignoreStructuralErrors = cxt.laxMode;
cxt.root = &jbv;
cxt.current = &jbv;
cxt.baseObject.jbc = NULL;
cxt.baseObject.id = 0;
- cxt.lastGeneratedObjectId = vars ? 2 : 1;
+ /* 1 + number of base objects in vars */
+ cxt.lastGeneratedObjectId = 1 + getVar(vars, NULL, 0, NULL, NULL);
cxt.innermostArraySize = -1;
cxt.throwErrors = throwErrors;
cxt.useTz = useTz;
@@ -2108,54 +2118,118 @@ getJsonPathItem(JsonPathExecContext *cxt, JsonPathItem *item,
&value->val.string.len);
break;
case jpiVariable:
- getJsonPathVariable(cxt, item, cxt->vars, value);
+ getJsonPathVariable(cxt, item, value);
return;
default:
elog(ERROR, "unexpected jsonpath item type");
}
}
+/*
+ * Returns the computed value of a JSON path variable with given name.
+ */
+static int
+GetJsonPathVar(void *cxt, char *varName, int varNameLen,
+ JsonbValue *val, JsonbValue *baseObject)
+{
+ JsonPathVariable *var = NULL;
+ List *vars = cxt;
+ ListCell *lc;
+ int id = 1;
+
+ if (!varName)
+ return list_length(vars);
+
+ foreach(lc, vars)
+ {
+ JsonPathVariable *curvar = lfirst(lc);
+
+ if (!strncmp(curvar->name, varName, varNameLen))
+ {
+ var = curvar;
+ break;
+ }
+
+ id++;
+ }
+
+ if (!var)
+ return -1;
+
+ if (var->isnull)
+ {
+ val->type = jbvNull;
+ return 0;
+ }
+
+ JsonItemFromDatum(var->value, var->typid, var->typmod, val);
+
+ *baseObject = *val;
+ return id;
+}
+
/*
* Get the value of variable passed to jsonpath executor
*/
static void
getJsonPathVariable(JsonPathExecContext *cxt, JsonPathItem *variable,
- Jsonb *vars, JsonbValue *value)
+ JsonbValue *value)
{
char *varName;
int varNameLength;
+ JsonbValue baseObject;
+ int baseObjectId;
+
+ Assert(variable->type == jpiVariable);
+ varName = jspGetString(variable, &varNameLength);
+
+ if (!cxt->vars ||
+ (baseObjectId = cxt->getVar(cxt->vars, varName, varNameLength, value,
+ &baseObject)) < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("could not find jsonpath variable \"%s\"",
+ pnstrdup(varName, varNameLength))));
+
+ if (baseObjectId > 0)
+ setBaseObject(cxt, &baseObject, baseObjectId);
+}
+
+static int
+getJsonPathVariableFromJsonb(void *varsJsonb, char *varName, int varNameLength,
+ JsonbValue *value, JsonbValue *baseObject)
+{
+ Jsonb *vars = varsJsonb;
JsonbValue tmp;
JsonbValue *v;
- if (!vars)
+ if (!varName)
{
- value->type = jbvNull;
- return;
+ if (vars && !JsonContainerIsObject(&vars->root))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"vars\" argument is not an object"),
+ errdetail("Jsonpath parameters should be encoded as key-value pairs of \"vars\" object.")));
+ }
+
+ return vars ? 1 : 0; /* count of base objects */
}
- Assert(variable->type == jpiVariable);
- varName = jspGetString(variable, &varNameLength);
tmp.type = jbvString;
tmp.val.string.val = varName;
tmp.val.string.len = varNameLength;
v = findJsonbValueFromContainer(&vars->root, JB_FOBJECT, &tmp);
- if (v)
- {
- *value = *v;
- pfree(v);
- }
- else
- {
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("could not find jsonpath variable \"%s\"",
- pnstrdup(varName, varNameLength))));
- }
+ if (!v)
+ return -1;
- JsonbInitBinary(&tmp, vars);
- setBaseObject(cxt, &tmp, 1);
+ *value = *v;
+ pfree(v);
+
+ JsonbInitBinary(baseObject, vars);
+ return 1;
}
/**************** Support functions for JsonPath execution *****************/
@@ -2812,3 +2886,240 @@ compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2,
return DatumGetInt32(DirectFunctionCall2(cmpfunc, val1, val2));
}
+
+/* Executor-callable JSON_EXISTS implementation */
+bool
+JsonPathExists(Datum jb, JsonPath *jp, bool *error, List *vars)
+{
+ JsonPathExecResult res = executeJsonPath(jp, vars, GetJsonPathVar,
+ DatumGetJsonbP(jb), !error, NULL,
+ true);
+
+ Assert(error || !jperIsError(res));
+
+ if (error && jperIsError(res))
+ *error = true;
+
+ return res == jperOk;
+}
+
+/* Executor-callable JSON_QUERY implementation */
+Datum
+JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty,
+ bool *error, List *vars)
+{
+ JsonbValue *first;
+ bool wrap;
+ JsonValueList found = {0};
+ JsonPathExecResult res PG_USED_FOR_ASSERTS_ONLY;
+ int count;
+
+ res = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb),
+ !error, &found, true);
+
+ Assert(error || !jperIsError(res));
+
+ if (error && jperIsError(res))
+ {
+ *error = true;
+ *empty = false;
+ return (Datum) 0;
+ }
+
+ count = JsonValueListLength(&found);
+
+ first = count > 0 ? JsonValueListHead(&found) : NULL;
+
+ if (!first)
+ wrap = false;
+ else if (wrapper == JSW_NONE)
+ wrap = false;
+ else if (wrapper == JSW_UNCONDITIONAL)
+ wrap = true;
+ else if (wrapper == JSW_CONDITIONAL)
+ wrap = count > 1 ||
+ IsAJsonbScalar(first) ||
+ (first->type == jbvBinary &&
+ JsonContainerIsScalar(first->val.binary.data));
+ else
+ {
+ elog(ERROR, "unrecognized json wrapper %d", wrapper);
+ wrap = false;
+ }
+
+ if (wrap)
+ return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found)));
+
+ if (count > 1)
+ {
+ if (error)
+ {
+ *error = true;
+ return (Datum) 0;
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM),
+ errmsg("JSON path expression in JSON_QUERY should return singleton item without wrapper"),
+ errhint("Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array.")));
+ }
+
+ if (first)
+ return JsonbPGetDatum(JsonbValueToJsonb(first));
+
+ *empty = true;
+ return PointerGetDatum(NULL);
+}
+
+/* Executor-callable JSON_VALUE implementation */
+JsonbValue *
+JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars)
+{
+ JsonbValue *res;
+ JsonValueList found = {0};
+ JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY;
+ int count;
+
+ jper = executeJsonPath(jp, vars, GetJsonPathVar, DatumGetJsonbP(jb),
+ !error, &found, true);
+
+ Assert(error || !jperIsError(jper));
+
+ if (error && jperIsError(jper))
+ {
+ *error = true;
+ *empty = false;
+ return NULL;
+ }
+
+ count = JsonValueListLength(&found);
+
+ *empty = (count == 0);
+
+ if (*empty)
+ return NULL;
+
+ if (count > 1)
+ {
+ if (error)
+ {
+ *error = true;
+ return NULL;
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_MORE_THAN_ONE_SQL_JSON_ITEM),
+ errmsg("JSON path expression in JSON_VALUE should return singleton scalar item")));
+ }
+
+ res = JsonValueListHead(&found);
+
+ if (res->type == jbvBinary &&
+ JsonContainerIsScalar(res->val.binary.data))
+ JsonbExtractScalar(res->val.binary.data, res);
+
+ if (!IsAJsonbScalar(res))
+ {
+ if (error)
+ {
+ *error = true;
+ return NULL;
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED),
+ errmsg("JSON path expression in JSON_VALUE should return singleton scalar item")));
+ }
+
+ if (res->type == jbvNull)
+ return NULL;
+
+ return res;
+}
+
+static void
+JsonbValueInitNumericDatum(JsonbValue *jbv, Datum num)
+{
+ jbv->type = jbvNumeric;
+ jbv->val.numeric = DatumGetNumeric(num);
+}
+
+void
+JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res)
+{
+ switch (typid)
+ {
+ case BOOLOID:
+ res->type = jbvBool;
+ res->val.boolean = DatumGetBool(val);
+ break;
+ case NUMERICOID:
+ JsonbValueInitNumericDatum(res, val);
+ break;
+ case INT2OID:
+ JsonbValueInitNumericDatum(res, DirectFunctionCall1(int2_numeric, val));
+ break;
+ case INT4OID:
+ JsonbValueInitNumericDatum(res, DirectFunctionCall1(int4_numeric, val));
+ break;
+ case INT8OID:
+ JsonbValueInitNumericDatum(res, DirectFunctionCall1(int8_numeric, val));
+ break;
+ case FLOAT4OID:
+ JsonbValueInitNumericDatum(res, DirectFunctionCall1(float4_numeric, val));
+ break;
+ case FLOAT8OID:
+ JsonbValueInitNumericDatum(res, DirectFunctionCall1(float8_numeric, val));
+ break;
+ case TEXTOID:
+ case VARCHAROID:
+ res->type = jbvString;
+ res->val.string.val = VARDATA_ANY(val);
+ res->val.string.len = VARSIZE_ANY_EXHDR(val);
+ break;
+ case DATEOID:
+ case TIMEOID:
+ case TIMETZOID:
+ case TIMESTAMPOID:
+ case TIMESTAMPTZOID:
+ res->type = jbvDatetime;
+ res->val.datetime.value = val;
+ res->val.datetime.typid = typid;
+ res->val.datetime.typmod = typmod;
+ res->val.datetime.tz = 0;
+ break;
+ case JSONBOID:
+ {
+ JsonbValue *jbv = res;
+ Jsonb *jb = DatumGetJsonbP(val);
+
+ if (JsonContainerIsScalar(&jb->root))
+ {
+ bool result PG_USED_FOR_ASSERTS_ONLY;
+
+ result = JsonbExtractScalar(&jb->root, jbv);
+ Assert(result);
+ }
+ else
+ JsonbInitBinary(jbv, jb);
+ break;
+ }
+ case JSONOID:
+ {
+ text *txt = DatumGetTextP(val);
+ char *str = text_to_cstring(txt);
+ Jsonb *jb;
+
+ jb = DatumGetJsonbP(DirectFunctionCall1(jsonb_in,
+ CStringGetDatum(str)));
+ pfree(str);
+
+ JsonItemFromDatum(JsonbPGetDatum(jb), JSONBOID, -1, res);
+ break;
+ }
+ default:
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("only datetime, bool, numeric, and text types can be casted to jsonpath types")));
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index ed7f40f053..a1745c89ff 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -476,6 +476,8 @@ static void get_const_expr(Const *constval, deparse_context *context,
int showtype);
static void get_const_collation(Const *constval, deparse_context *context);
static void get_json_format(JsonFormat *format, StringInfo buf);
+static void get_json_returning(JsonReturning *returning, StringInfo buf,
+ bool json_format_by_default);
static void get_json_constructor(JsonConstructorExpr *ctor,
deparse_context *context, bool showimplicit);
static void get_json_constructor_options(JsonConstructorExpr *ctor,
@@ -518,6 +520,8 @@ static char *generate_qualified_type_name(Oid typid);
static text *string_to_text(char *str);
static char *flatten_reloptions(Oid relid);
static void get_reloptions(StringInfo buf, Datum reloptions);
+static void get_json_path_spec(Node *path_spec, deparse_context *context,
+ bool showimplicit);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -8300,6 +8304,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
case T_WindowFunc:
case T_FuncExpr:
case T_JsonConstructorExpr:
+ case T_JsonExpr:
/* function-like: name(..) or name[..] */
return true;
@@ -8471,6 +8476,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
case T_GroupingFunc: /* own parentheses */
case T_WindowFunc: /* own parentheses */
case T_CaseExpr: /* other separators */
+ case T_JsonExpr: /* own parentheses */
return true;
default:
return false;
@@ -8586,6 +8592,64 @@ get_rule_expr_paren(Node *node, deparse_context *context,
appendStringInfoChar(context->buf, ')');
}
+static void
+get_json_behavior(JsonBehavior *behavior, deparse_context *context,
+ const char *on)
+{
+ /*
+ * The order of array elements must correspond to the order of
+ * JsonBehaviorType members.
+ */
+ const char *behavior_names[] =
+ {
+ " NULL",
+ " ERROR",
+ " EMPTY",
+ " TRUE",
+ " FALSE",
+ " UNKNOWN",
+ " EMPTY ARRAY",
+ " EMPTY OBJECT",
+ " DEFAULT "
+ };
+
+ if ((int) behavior->btype < 0 || behavior->btype >= lengthof(behavior_names))
+ elog(ERROR, "invalid json behavior type: %d", behavior->btype);
+
+ appendStringInfoString(context->buf, behavior_names[behavior->btype]);
+
+ if (behavior->btype == JSON_BEHAVIOR_DEFAULT)
+ get_rule_expr(behavior->expr, context, false);
+
+ appendStringInfo(context->buf, " ON %s", on);
+}
+
+/*
+ * get_json_expr_options
+ *
+ * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS.
+ */
+static void
+get_json_expr_options(JsonExpr *jsexpr, deparse_context *context,
+ JsonBehaviorType default_behavior)
+{
+ if (jsexpr->op == JSON_QUERY_OP)
+ {
+ if (jsexpr->wrapper == JSW_CONDITIONAL)
+ appendStringInfo(context->buf, " WITH CONDITIONAL WRAPPER");
+ else if (jsexpr->wrapper == JSW_UNCONDITIONAL)
+ appendStringInfo(context->buf, " WITH UNCONDITIONAL WRAPPER");
+
+ if (jsexpr->omit_quotes)
+ appendStringInfo(context->buf, " OMIT QUOTES");
+ }
+
+ if (jsexpr->on_empty && jsexpr->on_empty->btype != default_behavior)
+ get_json_behavior(jsexpr->on_empty, context, "EMPTY");
+
+ if (jsexpr->on_error && jsexpr->on_error->btype != default_behavior)
+ get_json_behavior(jsexpr->on_error, context, "ERROR");
+}
/* ----------
* get_rule_expr - Parse back an expression
@@ -9745,6 +9809,7 @@ get_rule_expr(Node *node, deparse_context *context,
}
break;
+
case T_JsonValueExpr:
{
JsonValueExpr *jve = (JsonValueExpr *) node;
@@ -9794,6 +9859,64 @@ get_rule_expr(Node *node, deparse_context *context,
}
break;
+ case T_JsonExpr:
+ {
+ JsonExpr *jexpr = (JsonExpr *) node;
+
+ switch (jexpr->op)
+ {
+ case JSON_EXISTS_OP:
+ appendStringInfoString(buf, "JSON_EXISTS(");
+ break;
+ case JSON_QUERY_OP:
+ appendStringInfoString(buf, "JSON_QUERY(");
+ break;
+ case JSON_VALUE_OP:
+ appendStringInfoString(buf, "JSON_VALUE(");
+ break;
+ }
+
+ get_rule_expr(jexpr->formatted_expr, context, showimplicit);
+
+ appendStringInfoString(buf, ", ");
+
+ get_json_path_spec(jexpr->path_spec, context, showimplicit);
+
+ if (jexpr->passing_values)
+ {
+ ListCell *lc1,
+ *lc2;
+ bool needcomma = false;
+
+ appendStringInfoString(buf, " PASSING ");
+
+ forboth(lc1, jexpr->passing_names,
+ lc2, jexpr->passing_values)
+ {
+ if (needcomma)
+ appendStringInfoString(buf, ", ");
+ needcomma = true;
+
+ get_rule_expr((Node *) lfirst(lc2), context, showimplicit);
+ appendStringInfo(buf, " AS %s",
+ ((String *) lfirst_node(String, lc1))->sval);
+ }
+ }
+
+ if (jexpr->op != JSON_EXISTS_OP ||
+ jexpr->returning->typid != BOOLOID)
+ get_json_returning(jexpr->returning, context->buf,
+ jexpr->op == JSON_QUERY_OP);
+
+ get_json_expr_options(jexpr, context,
+ jexpr->op != JSON_EXISTS_OP ?
+ JSON_BEHAVIOR_NULL :
+ JSON_BEHAVIOR_FALSE);
+
+ appendStringInfoString(buf, ")");
+ }
+ break;
+
case T_List:
{
char *sep;
@@ -9917,6 +10040,7 @@ looks_like_function(Node *node)
case T_MinMaxExpr:
case T_SQLValueFunction:
case T_XmlExpr:
+ case T_JsonExpr:
/* these are all accepted by func_expr_common_subexpr */
return true;
default:
@@ -10786,6 +10910,18 @@ get_const_collation(Const *constval, deparse_context *context)
}
}
+/*
+ * get_json_path_spec - Parse back a JSON path specification
+ */
+static void
+get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit)
+{
+ if (IsA(path_spec, Const))
+ get_const_expr((Const *) path_spec, context, -1);
+ else
+ get_rule_expr(path_spec, context, showimplicit);
+}
+
/*
* get_json_format - Parse back a JsonFormat node
*/
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index c4fd933154..aecb58664b 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -23,6 +23,8 @@ struct ExprEvalStep;
struct SubscriptingRefState;
struct ScalarArrayOpExprHashTable;
struct JsonConstructorExprState;
+struct JsonbValue;
+struct JsonExprState;
/* Bits in ExprState->flags (see also execnodes.h for public flag bits): */
/* expression's interpreter has been initialized */
@@ -240,6 +242,9 @@ typedef enum ExprEvalOp
EEOP_XMLEXPR,
EEOP_JSON_CONSTRUCTOR,
EEOP_IS_JSON,
+ EEOP_JSONEXPR_PATH,
+ EEOP_JSONEXPR_COERCION,
+ EEOP_JSONEXPR_COERCION_FINISH,
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
EEOP_WINDOW_FUNC,
@@ -692,6 +697,17 @@ typedef struct ExprEvalStep
JsonIsPredicate *pred; /* original expression node */
} is_json;
+ /* for EEOP_JSONEXPR_PATH */
+ struct
+ {
+ struct JsonExprState *jsestate;
+ } jsonexpr;
+
+ /* for EEOP_JSONEXPR_COERCION */
+ struct
+ {
+ struct JsonCoercionState *jcstate;
+ } jsonexpr_coercion;
} d;
} ExprEvalStep;
@@ -755,6 +771,118 @@ typedef struct JsonConstructorExprState
int nargs;
} JsonConstructorExprState;
+/*
+ * Information about the state of JsonPath* evaluation.
+ */
+typedef struct JsonExprPostEvalState
+{
+ /* Did JsonPath* evaluation cause an error? */
+ NullableDatum error;
+
+ /* Is the result of JsonPath* evaluation empty? */
+ NullableDatum empty;
+
+ /*
+ * ExecEvalJsonExprPath() will set this to the address of the step to
+ * use to coerce the result of JsonPath* evaluation to the RETURNING type.
+ * Also see the description of possible step addresses that this could be
+ * set to in the definition of JsonExprState.
+ */
+#define FIELDNO_JSONEXPRPOSTEVALSTATE_JUMP_EVAL_COERCION 2
+ int jump_eval_coercion;
+} JsonExprPostEvalState;
+
+/* State for evaluating a JsonExpr, too big to inline */
+typedef struct JsonExprState
+{
+ /* original expression node */
+ JsonExpr *jsexpr;
+
+ /* value/isnull for formatted_expr */
+ NullableDatum formatted_expr;
+
+ /* value/isnull for pathspec */
+ NullableDatum pathspec;
+
+ /* JsonPathVariable entries for passing_values */
+ List *args;
+
+ /*
+ * Per-row result status info populated by ExecEvalJsonExprPath()
+ * and ExecEvalJsonCoercionFinish().
+ */
+ JsonExprPostEvalState post_eval;
+
+ /*
+ * Address of the step that implements the non-ERROR variant of ON ERROR
+ * and ON EMPTY behaviors, to be jumped to when ExecEvalJsonExprPath()
+ * returns false on encountering an error during JsonPath* evaluation
+ * (ON ERROR) or on finding that no matching JSON item was returned (ON
+ * EMPTY). The same steps are also performed on encountering an error
+ * when coercing JsonPath* result to the RETURNING type.
+ */
+ int jump_error;
+
+ /*
+ * Addresses of steps to perform the coercion of the JsonPath* result value
+ * to the RETURNING type. Each address points to either 1) a special
+ * EEOP_JSONEXPR_COERCION step that handles coercion using the RETURNING
+ * type's input function or by using json_via_populate(), or 2) an
+ * expression such as CoerceViaIO. It may be -1 if no coercion is
+ * necessary.
+ *
+ * jump_eval_result_coercion points to the step to evaluate the coercion
+ * given in JsonExpr.result_coercion.
+ */
+ int jump_eval_result_coercion;
+
+ /* eval_item_coercion_jumps is an array of num_item_coercions elements
+ * each containing a step address to evaluate the coercion from a value of
+ * the given JsonItemType to the RETURNING type, or -1 if no coercion is
+ * necessary. item_coercion_via_expr is an array of boolean flags of the
+ * same length that indicates whether each valid step address in the
+ * eval_item_coercion_jumps array points to an expression or a
+ * EEOP_JSONEXPR_COERCION step. ExecEvalJsonExprPath() will cause an
+ * error if it's the latter, because that mode of coercion is not
+ * supported for all JsonItemTypes.
+ */
+ int num_item_coercions;
+ int *eval_item_coercion_jumps;
+ bool *item_coercion_via_expr;
+
+ /*
+ * For passing when initializing a EEOP_IOCOERCE_SAFE step for any
+ * CoerceViaIO nodes in the expression that must be evaluated in an
+ * error-safe manner.
+ */
+ ErrorSaveContext escontext;
+} JsonExprState;
+
+/*
+ * State for coercing a value to the target type specified in 'coercion' using
+ * either json_populate_type() or by calling the type's input function.
+ */
+typedef struct JsonCoercionState
+{
+ /* original expression node */
+ JsonCoercion *coercion;
+
+ /* Input function info for the target type. */
+ struct
+ {
+ FmgrInfo *finfo;
+ Oid typioparam;
+ } input;
+
+ /* Cache for json_populate_type() */
+ void *cache;
+
+ /*
+ * For soft-error handling in json_populate_type() or
+ * in InputFunctionCallSafe().
+ */
+ ErrorSaveContext *escontext;
+} JsonCoercionState;
/* functions in execExpr.c */
extern void ExprEvalPushStep(ExprState *es, const ExprEvalStep *s);
@@ -809,6 +937,11 @@ extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op);
extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalJsonCoercion(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
+extern void ExecEvalJsonCoercionFinish(ExprState *state, ExprEvalStep *op);
+extern bool ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op,
+ ExprContext *econtext);
extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index edf61e53f3..8a6b126de1 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -85,6 +85,7 @@ typedef struct FmgrInfo
typedef struct FunctionCallInfoBaseData
{
FmgrInfo *flinfo; /* ptr to lookup info used for this call */
+#define FIELDNO_FUNCTIONCALLINFODATA_CONTEXT 1
fmNodePtr context; /* pass info about context of call */
fmNodePtr resultinfo; /* pass or return extra info about result */
Oid fncollation; /* collation for function to use */
diff --git a/src/include/jit/llvmjit.h b/src/include/jit/llvmjit.h
index 3ab86de3ac..de9d8d36c5 100644
--- a/src/include/jit/llvmjit.h
+++ b/src/include/jit/llvmjit.h
@@ -85,6 +85,7 @@ extern PGDLLIMPORT LLVMTypeRef StructAggState;
extern PGDLLIMPORT LLVMTypeRef StructAggStatePerTransData;
extern PGDLLIMPORT LLVMTypeRef StructAggStatePerGroupData;
extern PGDLLIMPORT LLVMTypeRef StructPlanState;
+extern PGDLLIMPORT LLVMTypeRef StructJsonExprPostEvalState;
extern PGDLLIMPORT LLVMValueRef AttributeTemplate;
extern PGDLLIMPORT LLVMValueRef ExecEvalBoolSubroutineTemplate;
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 3180703005..f98578c92e 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -112,6 +112,8 @@ extern JsonFormat *makeJsonFormat(JsonFormatType type, JsonEncoding encoding,
int location);
extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr,
JsonFormat *format);
+extern JsonBehavior *makeJsonBehavior(JsonBehaviorType type, Node *expr,
+ JsonCoercion *coercion, int location);
extern Node *makeJsonKeyValue(Node *key, Node *value);
extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format,
JsonValueType item_type, bool unique_keys,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e494309da8..f0560c3737 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1692,6 +1692,23 @@ typedef struct TriggerTransition
/* Nodes for SQL/JSON support */
+/*
+ * JsonQuotes -
+ * representation of [KEEP|OMIT] QUOTES clause for JSON_QUERY()
+ */
+typedef enum JsonQuotes
+{
+ JS_QUOTES_UNSPEC, /* unspecified */
+ JS_QUOTES_KEEP, /* KEEP QUOTES */
+ JS_QUOTES_OMIT, /* OMIT QUOTES */
+} JsonQuotes;
+
+/*
+ * JsonPathSpec -
+ * representation of JSON path constant
+ */
+typedef char *JsonPathSpec;
+
/*
* JsonOutput -
* representation of JSON output clause (RETURNING type [FORMAT format])
@@ -1703,6 +1720,36 @@ typedef struct JsonOutput
JsonReturning *returning; /* RETURNING FORMAT clause and type Oids */
} JsonOutput;
+/*
+ * JsonArgument -
+ * representation of argument from JSON PASSING clause
+ */
+typedef struct JsonArgument
+{
+ NodeTag type;
+ JsonValueExpr *val; /* argument value expression */
+ char *name; /* argument name */
+} JsonArgument;
+
+/*
+ * JsonFuncExpr -
+ * untransformed representation of JSON function expressions
+ */
+typedef struct JsonFuncExpr
+{
+ NodeTag type;
+ JsonExprOp op; /* expression type */
+ JsonValueExpr *context_item; /* context item expression */
+ Node *pathspec; /* JSON path specification expression */
+ List *passing; /* list of PASSING clause arguments, if any */
+ JsonOutput *output; /* output clause, if specified */
+ List *behavior; /* ON EMPTY / ERROR behaviors, list of two
+ * JsonBehavior nodes in that order */
+ JsonWrapper wrapper; /* array wrapper behavior (JSON_QUERY only) */
+ JsonQuotes quotes; /* omit or keep quotes? (JSON_QUERY only) */
+ int location; /* token location, or -1 if unknown */
+} JsonFuncExpr;
+
/*
* JsonKeyValue -
* untransformed representation of JSON object key-value pair for
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb930afb52..7504b09ca8 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1552,6 +1552,17 @@ typedef struct XmlExpr
int location;
} XmlExpr;
+/*
+ * JsonExprOp -
+ * enumeration of JSON functions using JSON path
+ */
+typedef enum JsonExprOp
+{
+ JSON_VALUE_OP, /* JSON_VALUE() */
+ JSON_QUERY_OP, /* JSON_QUERY() */
+ JSON_EXISTS_OP, /* JSON_EXISTS() */
+} JsonExprOp;
+
/*
* JsonEncoding -
* representation of JSON ENCODING clause
@@ -1576,6 +1587,37 @@ typedef enum JsonFormatType
* jsonb */
} JsonFormatType;
+/*
+ * JsonBehaviorType -
+ * enumeration of behavior types used in JSON ON ... BEHAVIOR clause
+ *
+ * If enum members are reordered, get_json_behavior() from ruleutils.c
+ * must be updated accordingly.
+ */
+typedef enum JsonBehaviorType
+{
+ JSON_BEHAVIOR_NULL = 0,
+ JSON_BEHAVIOR_ERROR,
+ JSON_BEHAVIOR_EMPTY,
+ JSON_BEHAVIOR_TRUE,
+ JSON_BEHAVIOR_FALSE,
+ JSON_BEHAVIOR_UNKNOWN,
+ JSON_BEHAVIOR_EMPTY_ARRAY,
+ JSON_BEHAVIOR_EMPTY_OBJECT,
+ JSON_BEHAVIOR_DEFAULT,
+} JsonBehaviorType;
+
+/*
+ * JsonWrapper -
+ * representation of WRAPPER clause for JSON_QUERY()
+ */
+typedef enum JsonWrapper
+{
+ JSW_NONE,
+ JSW_CONDITIONAL,
+ JSW_UNCONDITIONAL,
+} JsonWrapper;
+
/*
* JsonFormat -
* representation of JSON FORMAT clause
@@ -1670,6 +1712,94 @@ typedef struct JsonIsPredicate
int location; /* token location, or -1 if unknown */
} JsonIsPredicate;
+/*
+ * JsonItemType
+ * Represents type codes to identify a JsonCoercion node to use when
+ * coercing a given SQL/JSON items to the output SQL type
+ *
+ * The comment next to each item type mentions the JsonbValue.jbvType of the
+ * source JsonbValue value to be coerced using the expression in the
+ * JsonCoercion node.
+ *
+ * Also, see InitJsonItemCoercions() and ExecPrepareJsonItemCoercion().
+ */
+typedef enum JsonItemType
+{
+ JsonItemTypeNull = 0, /* jbvNull */
+ JsonItemTypeString = 1, /* jbvString */
+ JsonItemTypeNumeric = 2, /* jbvNumeric */
+ JsonItemTypeBoolean = 3, /* jbvBool */
+ JsonItemTypeDate = 4, /* jbvDatetime: DATEOID */
+ JsonItemTypeTime = 5, /* jbvDatetime: TIMEOID */
+ JsonItemTypeTimetz = 6, /* jbvDatetime: TIMETZOID */
+ JsonItemTypeTimestamp = 7, /* jbvDatetime: TIMESTAMPOID */
+ JsonItemTypeTimestamptz = 8, /* jbvDatetime: TIMESTAMPTZOID */
+ JsonItemTypeComposite = 9, /* jbvArray, jbvObject, jbvBinary */
+ JsonItemTypeInvalid = 10,
+} JsonItemType;
+
+/*
+ * JsonCoercion -
+ * coercion from SQL/JSON item types to SQL types
+ */
+typedef struct JsonCoercion
+{
+ NodeTag type;
+
+ Oid targettype;
+ int32 targettypmod;
+ bool via_populate; /* coerce result using json_populate_type()? */
+ bool via_io; /* coerce result using type input function? */
+ Oid collation; /* collation for coercion via I/O or populate */
+} JsonCoercion;
+
+typedef struct JsonItemCoercion
+{
+ NodeTag type;
+
+ JsonItemType item_type;
+ Node *coercion;
+} JsonItemCoercion;
+
+/*
+ * JsonBehavior -
+ * representation of a given JSON behavior
+ */
+typedef struct JsonBehavior
+{
+ NodeTag type;
+
+ JsonBehaviorType btype; /* behavior type */
+ Node *expr; /* behavior expression */
+ JsonCoercion *coercion; /* to coerce behavior expression when there is
+ * no cast to the target type */
+ int location; /* token location, or -1 if unknown */
+} JsonBehavior;
+
+/*
+ * JsonExpr -
+ * transformed representation of JSON_VALUE(), JSON_QUERY(), JSON_EXISTS()
+ */
+typedef struct JsonExpr
+{
+ Expr xpr;
+
+ JsonExprOp op; /* json function ID */
+ Node *formatted_expr; /* formatted context item expression */
+ Node *result_coercion; /* resulting coercion to RETURNING type */
+ JsonFormat *format; /* context item format (JSON/JSONB) */
+ Node *path_spec; /* JSON path specification expression */
+ List *passing_names; /* PASSING argument names */
+ List *passing_values; /* PASSING argument values */
+ JsonReturning *returning; /* RETURNING clause type/format info */
+ JsonBehavior *on_empty; /* ON EMPTY behavior */
+ JsonBehavior *on_error; /* ON ERROR behavior */
+ List *item_coercions; /* coercions for JSON_VALUE */
+ JsonWrapper wrapper; /* WRAPPER for JSON_QUERY */
+ bool omit_quotes; /* KEEP/OMIT QUOTES for JSON_QUERY */
+ int location; /* token location, or -1 if unknown */
+} JsonExpr;
+
/* ----------------
* NullTest
*
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 5984dcfa4b..0954d9fc7b 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -93,6 +93,7 @@ PG_KEYWORD("commit", COMMIT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("committed", COMMITTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("compression", COMPRESSION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("concurrently", CONCURRENTLY, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("conditional", CONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("configuration", CONFIGURATION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("conflict", CONFLICT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("connection", CONNECTION, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -147,11 +148,13 @@ PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("event", EVENT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("except", EXCEPT, RESERVED_KEYWORD, AS_LABEL)
@@ -233,10 +236,14 @@ PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json", JSON, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_array", JSON_ARRAY, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_arrayagg", JSON_ARRAYAGG, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("json_exists", JSON_EXISTS, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_object", JSON_OBJECT, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL)
PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -302,6 +309,7 @@ PG_KEYWORD("off", OFF, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("offset", OFFSET, RESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("oids", OIDS, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("old", OLD, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("omit", OMIT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("on", ON, RESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("only", ONLY, RESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("operator", OPERATOR, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -344,6 +352,7 @@ PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("read", READ, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("real", REAL, COL_NAME_KEYWORD, BARE_LABEL)
@@ -414,6 +423,7 @@ PG_KEYWORD("stdout", STDOUT, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("storage", STORAGE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("string", STRING_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL)
@@ -449,6 +459,7 @@ PG_KEYWORD("types", TYPES_P, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("uescape", UESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("unbounded", UNBOUNDED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("uncommitted", UNCOMMITTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("unconditional", UNCONDITIONAL, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("unencrypted", UNENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
PG_KEYWORD("union", UNION, RESERVED_KEYWORD, AS_LABEL)
PG_KEYWORD("unique", UNIQUE, RESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h
index 0cad3a2709..9a9ae9754e 100644
--- a/src/include/utils/formatting.h
+++ b/src/include/utils/formatting.h
@@ -29,5 +29,6 @@ extern char *asc_initcap(const char *buff, size_t nbytes);
extern Datum parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict,
Oid *typid, int32 *typmod, int *tz,
struct Node *escontext);
+extern bool datetime_format_has_tz(const char *fmt_str);
#endif
diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h
index addc9b608e..613d5953f2 100644
--- a/src/include/utils/jsonb.h
+++ b/src/include/utils/jsonb.h
@@ -422,6 +422,7 @@ extern char *JsonbToCString(StringInfo out, JsonbContainer *in,
int estimated_len);
extern char *JsonbToCStringIndent(StringInfo out, JsonbContainer *in,
int estimated_len);
+extern char *JsonbUnquote(Jsonb *jb);
extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res);
extern const char *JsonbTypeName(JsonbValue *val);
diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h
index 1c6d2be025..4c41eb5540 100644
--- a/src/include/utils/jsonfuncs.h
+++ b/src/include/utils/jsonfuncs.h
@@ -15,6 +15,7 @@
#define JSONFUNCS_H
#include "common/jsonapi.h"
+#include "nodes/nodes.h"
#include "utils/jsonb.h"
/*
@@ -87,5 +88,9 @@ extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory,
extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory,
Oid outfuncoid);
extern Datum jsonb_from_text(text *js, bool unique_keys);
+extern Datum json_populate_type(Datum json_val, Oid json_type,
+ Oid typid, int32 typmod,
+ void **cache, MemoryContext mcxt, bool *isnull,
+ Node *escontext);
#endif
diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h
index f0181e045f..5a37133847 100644
--- a/src/include/utils/jsonpath.h
+++ b/src/include/utils/jsonpath.h
@@ -16,7 +16,9 @@
#include "fmgr.h"
#include "nodes/pg_list.h"
+#include "nodes/primnodes.h"
#include "utils/jsonb.h"
+#include "utils/jsonfuncs.h"
typedef struct
{
@@ -184,6 +186,7 @@ extern bool jspGetBool(JsonPathItem *v);
extern char *jspGetString(JsonPathItem *v, int32 *len);
extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from,
JsonPathItem *to, int i);
+extern bool jspIsMutable(JsonPath *path, List *varnames, List *varexprs);
extern const char *jspOperationName(JsonPathItemType type);
@@ -261,4 +264,28 @@ extern bool jspConvertRegexFlags(uint32 xflags, int *result,
struct Node *escontext);
+/*
+ * Evaluation of jsonpath
+ */
+
+/* External variable passed into jsonpath. */
+typedef struct JsonPathVariable
+{
+ char *name;
+ Oid typid;
+ int32 typmod;
+ Datum value;
+ bool isnull;
+} JsonPathVariable;
+
+/* SQL/JSON item */
+extern void JsonItemFromDatum(Datum val, Oid typid, int32 typmod,
+ JsonbValue *res);
+
+extern bool JsonPathExists(Datum jb, JsonPath *path, bool *error, List *vars);
+extern Datum JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper,
+ bool *empty, bool *error, List *vars);
+extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty,
+ bool *error, List *vars);
+
#endif
diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer
index 435c139ec2..b2aa44f36d 100644
--- a/src/interfaces/ecpg/preproc/ecpg.trailer
+++ b/src/interfaces/ecpg/preproc/ecpg.trailer
@@ -651,6 +651,34 @@ var_type: simple_type
$$.type_index = mm_strdup("-1");
$$.type_sizeof = NULL;
}
+ | STRING_P
+ {
+ if (INFORMIX_MODE)
+ {
+ /* In Informix mode, "string" is automatically a typedef */
+ $$.type_enum = ECPGt_string;
+ $$.type_str = mm_strdup("char");
+ $$.type_dimension = mm_strdup("-1");
+ $$.type_index = mm_strdup("-1");
+ $$.type_sizeof = NULL;
+ }
+ else
+ {
+ /* Otherwise, legal only if user typedef'ed it */
+ struct typedefs *this = get_typedef("string", false);
+
+ $$.type_str = (this->type->type_enum == ECPGt_varchar || this->type->type_enum == ECPGt_bytea) ? EMPTY : mm_strdup(this->name);
+ $$.type_enum = this->type->type_enum;
+ $$.type_dimension = this->type->type_dimension;
+ $$.type_index = this->type->type_index;
+ if (this->type->type_sizeof && strlen(this->type->type_sizeof) != 0)
+ $$.type_sizeof = this->type->type_sizeof;
+ else
+ $$.type_sizeof = cat_str(3, mm_strdup("sizeof("), mm_strdup(this->name), mm_strdup(")"));
+
+ struct_member_list[struct_level] = ECPGstruct_member_dup(this->struct_member_list);
+ }
+ }
| INTERVAL ecpg_interval
{
$$.type_enum = ECPGt_interval;
diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out
new file mode 100644
index 0000000000..04d5cc74e3
--- /dev/null
+++ b/src/test/regress/expected/json_sqljson.out
@@ -0,0 +1,18 @@
+-- JSON_EXISTS
+SELECT JSON_EXISTS(NULL FORMAT JSON, '$');
+ERROR: JSON_EXISTS() is not yet implemented for the json type
+LINE 1: SELECT JSON_EXISTS(NULL FORMAT JSON, '$');
+ ^
+HINT: Try casting the argument to jsonb
+-- JSON_VALUE
+SELECT JSON_VALUE(NULL FORMAT JSON, '$');
+ERROR: JSON_VALUE() is not yet implemented for the json type
+LINE 1: SELECT JSON_VALUE(NULL FORMAT JSON, '$');
+ ^
+HINT: Try casting the argument to jsonb
+-- JSON_QUERY
+SELECT JSON_QUERY(NULL FORMAT JSON, '$');
+ERROR: JSON_QUERY() is not yet implemented for the json type
+LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$');
+ ^
+HINT: Try casting the argument to jsonb
diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out
new file mode 100644
index 0000000000..cb727e930a
--- /dev/null
+++ b/src/test/regress/expected/jsonb_sqljson.out
@@ -0,0 +1,1032 @@
+-- JSON_EXISTS
+SELECT JSON_EXISTS(NULL::jsonb, '$');
+ json_exists
+-------------
+
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '[]', '$');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', '$');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb 'null', '$');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '[]', '$');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', '$.a');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); -- FALSE on error
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR);
+ERROR: jsonpath member accessor can only be applied to an object
+SELECT JSON_EXISTS(jsonb 'null', '$.a');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '[]', '$.a');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); -- FALSE on error
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{}', '$.a');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', '$.a.b');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b');
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x);
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x);
+ json_exists
+-------------
+ f
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y);
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y);
+ json_exists
+-------------
+ f
+(1 row)
+
+-- extension: boolean expressions
+SELECT JSON_EXISTS(jsonb '1', '$ > 2');
+ json_exists
+-------------
+ t
+(1 row)
+
+SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR);
+ json_exists
+-------------
+ t
+(1 row)
+
+-- JSON_VALUE
+SELECT JSON_VALUE(NULL::jsonb, '$');
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$');
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int);
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'true', '$');
+ json_value
+------------
+ true
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool);
+ json_value
+------------
+ t
+(1 row)
+
+SELECT JSON_VALUE(jsonb '123', '$');
+ json_value
+------------
+ 123
+(1 row)
+
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234;
+ ?column?
+----------
+ 357
+(1 row)
+
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING text);
+ json_value
+------------
+ 123
+(1 row)
+
+/* jsonb bytea ??? */
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR);
+ERROR: SQL/JSON item cannot be cast to target type
+SELECT JSON_VALUE(jsonb '1.23', '$');
+ json_value
+------------
+ 1.23
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int);
+ json_value
+------------
+ 1
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric);
+ json_value
+------------
+ 1.23
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR);
+ERROR: invalid input syntax for type integer: "1.23"
+SELECT JSON_VALUE(jsonb '"aaa"', '$');
+ json_value
+------------
+ aaa
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text);
+ json_value
+------------
+ aaa
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5));
+ json_value
+------------
+ aaa
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2));
+ json_value
+------------
+ aa
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json);
+ json_value
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb);
+ json_value
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR);
+ json_value
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR);
+ json_value
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json);
+ json_value
+------------
+ "\"aaa\""
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb);
+ json_value
+------------
+ "\"aaa\""
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int);
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR);
+ERROR: invalid input syntax for type integer: "aaa"
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR);
+ json_value
+------------
+ 111
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234;
+ ?column?
+----------
+ 357
+(1 row)
+
+SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9;
+ ?column?
+------------
+ 03-01-2017
+(1 row)
+
+-- Test NULL checks execution in domain types
+CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL;
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null);
+ERROR: domain sqljsonb_int_not_null does not allow null values
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null ERROR ON ERROR);
+ERROR: domain sqljsonb_int_not_null does not allow null values
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR);
+ERROR: domain sqljsonb_int_not_null does not allow null values
+SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR);
+ json_value
+------------
+ 2
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON EMPTY ERROR ON ERROR);
+ERROR: domain sqljsonb_int_not_null does not allow null values
+CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple');
+CREATE DOMAIN rgb AS rainbow CHECK (VALUE IN ('red', 'green', 'blue'));
+SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb);
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb ERROR ON ERROR);
+ERROR: value for domain rgb violates check constraint "rgb_check"
+SELECT JSON_VALUE(jsonb '[]', '$');
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR);
+ERROR: JSON path expression in JSON_VALUE should return singleton scalar item
+SELECT JSON_VALUE(jsonb '{}', '$');
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR);
+ERROR: JSON path expression in JSON_VALUE should return singleton scalar item
+SELECT JSON_VALUE(jsonb '1', '$.a');
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR);
+ERROR: jsonpath member accessor can only be applied to an object
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR);
+ json_value
+------------
+ error
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR);
+ json_value
+------------
+ 2
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR);
+ json_value
+------------
+ 2
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR);
+ json_value
+------------
+ 2
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR);
+ json_value
+------------
+
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR);
+ json_value
+------------
+ 2
+(1 row)
+
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR);
+ERROR: JSON path expression in JSON_VALUE should return singleton scalar item
+SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR);
+ json_value
+------------
+ 0
+(1 row)
+
+SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR);
+ERROR: invalid input syntax for type integer: " "
+SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
+ json_value
+------------
+ 5
+(1 row)
+
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
+ json_value
+------------
+ 1
+(1 row)
+
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed
+ERROR: cannot specify FORMAT in RETURNING clause of JSON_VALUE()
+LINE 1: ...CT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSO...
+ ^
+-- RETUGNING pseudo-types not allowed
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record);
+ERROR: returning pseudo-types is not supported in SQL/JSON functions
+SELECT
+ x,
+ JSON_VALUE(
+ jsonb '{"a": 1, "b": 2}',
+ '$.* ? (@ > $x)' PASSING x AS x
+ RETURNING int
+ DEFAULT -1 ON EMPTY
+ DEFAULT -2 ON ERROR
+ ) y
+FROM
+ generate_series(0, 2) x;
+ x | y
+---+----
+ 0 | -2
+ 1 | 2
+ 2 | -1
+(3 rows)
+
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a);
+ json_value
+------------
+ (1,2)
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point);
+ json_value
+------------
+ (1,2)
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point ERROR ON ERROR);
+ json_value
+------------
+ (1,2)
+(1 row)
+
+-- Test PASSING and RETURNING date/time types
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts);
+ json_value
+------------------------------
+ Tue Feb 20 18:34:56 2018 PST
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz);
+ json_value
+------------------------------
+ Tue Feb 20 18:34:56 2018 PST
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp);
+ json_value
+--------------------------
+ Tue Feb 20 18:34:56 2018
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING date '2018-02-21 12:34:56 +10' AS ts RETURNING date);
+ json_value
+------------
+ 02-21-2018
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING time '2018-02-21 12:34:56 +10' AS ts RETURNING time);
+ json_value
+------------
+ 12:34:56
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timetz '2018-02-21 12:34:56 +10' AS ts RETURNING timetz);
+ json_value
+-------------
+ 12:34:56+10
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp);
+ json_value
+--------------------------
+ Wed Feb 21 12:34:56 2018
+(1 row)
+
+-- Also test RETURNING json[b]
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json);
+ json_value
+-----------------------------
+ "2018-02-21T02:34:56+00:00"
+(1 row)
+
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb);
+ json_value
+-----------------------------
+ "2018-02-21T02:34:56+00:00"
+(1 row)
+
+-- JSON_QUERY
+SELECT
+ JSON_QUERY(js, '$'),
+ JSON_QUERY(js, '$' WITHOUT WRAPPER),
+ JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER),
+ JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER),
+ JSON_QUERY(js, '$' WITH ARRAY WRAPPER)
+FROM
+ (VALUES
+ (jsonb 'null'),
+ ('12.3'),
+ ('true'),
+ ('"aaa"'),
+ ('[1, null, "2"]'),
+ ('{"a": 1, "b": [2]}')
+ ) foo(js);
+ json_query | json_query | json_query | json_query | json_query
+--------------------+--------------------+--------------------+----------------------+----------------------
+ null | null | [null] | [null] | [null]
+ 12.3 | 12.3 | [12.3] | [12.3] | [12.3]
+ true | true | [true] | [true] | [true]
+ "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"]
+ [1, null, "2"] | [1, null, "2"] | [1, null, "2"] | [[1, null, "2"]] | [[1, null, "2"]]
+ {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}]
+(6 rows)
+
+SELECT
+ JSON_QUERY(js, 'strict $[*]') AS "unspec",
+ JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without",
+ JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond",
+ JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond",
+ JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with"
+FROM
+ (VALUES
+ (jsonb '1'),
+ ('[]'),
+ ('[null]'),
+ ('[12.3]'),
+ ('[true]'),
+ ('["aaa"]'),
+ ('[[1, 2, 3]]'),
+ ('[{"a": 1, "b": [2]}]'),
+ ('[1, "2", null, [3]]')
+ ) foo(js);
+ unspec | without | with cond | with uncond | with
+--------------------+--------------------+---------------------+----------------------+----------------------
+ | | | |
+ | | | |
+ null | null | [null] | [null] | [null]
+ 12.3 | 12.3 | [12.3] | [12.3] | [12.3]
+ true | true | [true] | [true] | [true]
+ "aaa" | "aaa" | ["aaa"] | ["aaa"] | ["aaa"]
+ [1, 2, 3] | [1, 2, 3] | [1, 2, 3] | [[1, 2, 3]] | [[1, 2, 3]]
+ {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | {"a": 1, "b": [2]} | [{"a": 1, "b": [2]}] | [{"a": 1, "b": [2]}]
+ | | [1, "2", null, [3]] | [1, "2", null, [3]] | [1, "2", null, [3]]
+(9 rows)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text);
+ json_query
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES);
+ json_query
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING);
+ json_query
+------------
+ "aaa"
+(1 row)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES);
+ json_query
+------------
+ aaa
+(1 row)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING);
+ json_query
+------------
+ aaa
+(1 row)
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR);
+ERROR: invalid input syntax for type json
+DETAIL: Token "aaa" is invalid.
+CONTEXT: JSON data, line 1: aaa
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR);
+ERROR: invalid input syntax for type json
+DETAIL: Token "aaa" is invalid.
+CONTEXT: JSON data, line 1: aaa
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR);
+ json_query
+------------
+ \x616161
+(1 row)
+
+-- QUOTES behavior should not be specified when WITH WRAPPER used:
+-- Should fail
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES);
+ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used
+LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES)...
+ ^
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES);
+ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used
+LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES)...
+ ^
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES);
+ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used
+LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER ...
+ ^
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES);
+ERROR: SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used
+LINE 1: SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER ...
+ ^
+-- Should succeed
+SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES);
+ json_query
+------------
+ [1]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES);
+ json_query
+------------
+ [1]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]');
+ json_query
+------------
+
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY);
+ json_query
+------------
+
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY);
+ json_query
+------------
+ []
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY);
+ json_query
+------------
+ []
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY);
+ json_query
+------------
+ {}
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY);
+ json_query
+------------
+ "empty"
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR);
+ERROR: no SQL/JSON item
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR);
+ERROR: JSON path expression in JSON_QUERY should return singleton item without wrapper
+HINT: Use WITH WRAPPER clause to wrap SQL/JSON item sequence into array.
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR);
+ json_query
+------------
+ "empty"
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10));
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3));
+ json_query
+------------
+ [1,
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON);
+ json_query
+------------
+ [1, 2]
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea);
+ json_query
+----------------
+ \x5b312c20325d
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON);
+ json_query
+----------------
+ \x5b312c20325d
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR);
+ERROR: cannot cast behavior expression of type jsonb to bytea
+LINE 1: ... JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJE...
+ ^
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR);
+ERROR: cannot cast behavior expression of type jsonb to bytea
+LINE 1: ...jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJE...
+ ^
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR);
+ json_query
+------------
+ {}
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR);
+ json_query
+------------
+ {}
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR);
+ERROR: cannot cast behavior expression of type jsonb to bigint[]
+LINE 1: ...ON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJE...
+ ^
+SELECT JSON_QUERY(jsonb '"[3,4]"', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR);
+ERROR: cannot cast behavior expression of type jsonb to bigint[]
+LINE 1: ..._QUERY(jsonb '"[3,4]"', '$[*]' RETURNING bigint[] EMPTY OBJE...
+ ^
+-- RETUGNING pseudo-types not allowed
+SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING anyarray EMPTY OBJECT ON ERROR);
+ERROR: returning pseudo-types is not supported in SQL/JSON functions
+SELECT
+ x, y,
+ JSON_QUERY(
+ jsonb '[1,2,3,4,5,null]',
+ '$[*] ? (@ >= $x && @ <= $y)'
+ PASSING x AS x, y AS y
+ WITH CONDITIONAL WRAPPER
+ EMPTY ARRAY ON EMPTY
+ ) list
+FROM
+ generate_series(0, 4) x,
+ generate_series(0, 4) y;
+ x | y | list
+---+---+--------------
+ 0 | 0 | []
+ 0 | 1 | [1]
+ 0 | 2 | [1, 2]
+ 0 | 3 | [1, 2, 3]
+ 0 | 4 | [1, 2, 3, 4]
+ 1 | 0 | []
+ 1 | 1 | [1]
+ 1 | 2 | [1, 2]
+ 1 | 3 | [1, 2, 3]
+ 1 | 4 | [1, 2, 3, 4]
+ 2 | 0 | []
+ 2 | 1 | []
+ 2 | 2 | [2]
+ 2 | 3 | [2, 3]
+ 2 | 4 | [2, 3, 4]
+ 3 | 0 | []
+ 3 | 1 | []
+ 3 | 2 | []
+ 3 | 3 | [3]
+ 3 | 4 | [3, 4]
+ 4 | 0 | []
+ 4 | 1 | []
+ 4 | 2 | []
+ 4 | 3 | []
+ 4 | 4 | [4]
+(25 rows)
+
+-- Extension: record types returning
+CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]);
+CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]);
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec);
+ json_query
+-----------------------------------------------------
+ (1,aaa,"[1, ""2"", {}]","{""x"": [1, ""2"", {}]}",)
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[{"a": "a", "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec ERROR ON ERROR);
+ERROR: invalid input syntax for type integer: "a"
+SELECT JSON_QUERY(jsonb '[{"a": "a", "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec);
+ json_query
+------------
+
+(1 row)
+
+SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa);
+ unnest
+------------------------
+ {"a": 1, "b": ["foo"]}
+ {"a": 2, "c": {}}
+ 123
+(3 rows)
+
+SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca);
+ a | t | js | jb | jsa
+---+-------------+----+------------+-----
+ 1 | ["foo", []] | | |
+ 2 | | | [{}, true] |
+(2 rows)
+
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath);
+ json_query
+------------
+
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath ERROR ON ERROR);
+ERROR: syntax error at or near "{" of jsonpath input
+-- Extension: array types returning
+SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER);
+ json_query
+--------------
+ {1,2,NULL,3}
+(1 row)
+
+SELECT JSON_QUERY(jsonb '[1,2,null,"a"]', '$[*]' RETURNING int[] WITH WRAPPER ERROR ON ERROR);
+ERROR: invalid input syntax for type integer: "a"
+SELECT JSON_QUERY(jsonb '[1,2,null,"a"]', '$[*]' RETURNING int[] WITH WRAPPER);
+ json_query
+------------
+
+(1 row)
+
+SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[]));
+ a | t | js | jb | jsa
+---+-------------+----+------------+-----
+ 1 | ["foo", []] | | |
+ 2 | | | [{}, true] |
+(2 rows)
+
+-- Extension: domain types returning
+SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null);
+ json_query
+------------
+ 1
+(1 row)
+
+SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null);
+ERROR: domain sqljsonb_int_not_null does not allow null values
+-- Test timestamptz passing and output
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts);
+ json_query
+-----------------------------
+ "2018-02-21T02:34:56+00:00"
+(1 row)
+
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json);
+ json_query
+-----------------------------
+ "2018-02-21T02:34:56+00:00"
+(1 row)
+
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb);
+ json_query
+-----------------------------
+ "2018-02-21T02:34:56+00:00"
+(1 row)
+
+-- Test constraints
+CREATE TABLE test_jsonb_constraints (
+ js text,
+ i int,
+ x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
+ CONSTRAINT test_jsonb_constraint1
+ CHECK (js IS JSON)
+ CONSTRAINT test_jsonb_constraint2
+ CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr))
+ CONSTRAINT test_jsonb_constraint3
+ CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT '12' ON EMPTY ERROR ON ERROR) > i)
+ CONSTRAINT test_jsonb_constraint4
+ CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]')
+ CONSTRAINT test_jsonb_constraint5
+ CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C")
+);
+\d test_jsonb_constraints
+ Table "public.test_jsonb_constraints"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+--------------------------------------------------------------------------------
+ js | text | | |
+ i | integer | | |
+ x | jsonb | | | JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER)
+Check constraints:
+ "test_jsonb_constraint1" CHECK (js IS JSON)
+ "test_jsonb_constraint2" CHECK (JSON_EXISTS(js::jsonb, '$."a"' PASSING i + 5 AS int, i::text AS txt, ARRAY[1, 2, 3] AS arr))
+ "test_jsonb_constraint3" CHECK (JSON_VALUE(js::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i)
+ "test_jsonb_constraint4" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)
+ "test_jsonb_constraint5" CHECK (JSON_QUERY(js::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
+
+SELECT check_clause
+FROM information_schema.check_constraints
+WHERE constraint_name LIKE 'test_jsonb_constraint%'
+ORDER BY 1;
+ check_clause
+------------------------------------------------------------------------------------------------------------------------
+ (JSON_QUERY((js)::jsonb, '$."a"' RETURNING character(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > ('a'::bpchar COLLATE "C"))
+ (JSON_QUERY((js)::jsonb, '$."a"' RETURNING jsonb WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < '[10]'::jsonb)
+ (JSON_VALUE((js)::jsonb, '$."a"' RETURNING integer DEFAULT 12 ON EMPTY ERROR ON ERROR) > i)
+ (js IS JSON)
+ JSON_EXISTS((js)::jsonb, '$."a"' PASSING (i + 5) AS int, (i)::text AS txt, ARRAY[1, 2, 3] AS arr)
+(5 rows)
+
+SELECT pg_get_expr(adbin, adrelid)
+FROM pg_attrdef
+WHERE adrelid = 'test_jsonb_constraints'::regclass
+ORDER BY 1;
+ pg_get_expr
+--------------------------------------------------------------------------------
+ JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER)
+(1 row)
+
+SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass;
+ pg_get_expr
+--------------------------------------------------------------------------------
+ JSON_QUERY('[1, 2]'::jsonb, '$[*]' RETURNING jsonb WITH UNCONDITIONAL WRAPPER)
+(1 row)
+
+INSERT INTO test_jsonb_constraints VALUES ('', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint1"
+DETAIL: Failing row contains (, 1, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('1', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2"
+DETAIL: Failing row contains (1, 1, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('[]');
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2"
+DETAIL: Failing row contains ([], null, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint2"
+DETAIL: Failing row contains ({"b": 1}, 1, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint3"
+DETAIL: Failing row contains ({"a": 1}, 1, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint5"
+DETAIL: Failing row contains ({"a": 7}, 1, [1, 2]).
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1);
+ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4"
+DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]).
+DROP TABLE test_jsonb_constraints;
+-- Test mutabilily od query functions
+CREATE TABLE test_jsonb_mutability(js jsonb);
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())'));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))'));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))'));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x));
+ERROR: functions in index expression must be marked IMMUTABLE
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x));
+DROP TABLE test_jsonb_mutability;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f0987ff537..864bf04fe7 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
# ----------
# Another group of parallel tests (JSON related)
# ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson json_sqljson jsonb_sqljson
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql
new file mode 100644
index 0000000000..4f30fa46b9
--- /dev/null
+++ b/src/test/regress/sql/json_sqljson.sql
@@ -0,0 +1,11 @@
+-- JSON_EXISTS
+
+SELECT JSON_EXISTS(NULL FORMAT JSON, '$');
+
+-- JSON_VALUE
+
+SELECT JSON_VALUE(NULL FORMAT JSON, '$');
+
+-- JSON_QUERY
+
+SELECT JSON_QUERY(NULL FORMAT JSON, '$');
diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql
new file mode 100644
index 0000000000..77bfb6d61b
--- /dev/null
+++ b/src/test/regress/sql/jsonb_sqljson.sql
@@ -0,0 +1,337 @@
+-- JSON_EXISTS
+SELECT JSON_EXISTS(NULL::jsonb, '$');
+
+SELECT JSON_EXISTS(jsonb '[]', '$');
+SELECT JSON_EXISTS(JSON_OBJECT(RETURNING jsonb), '$');
+
+SELECT JSON_EXISTS(jsonb '1', '$');
+SELECT JSON_EXISTS(jsonb 'null', '$');
+SELECT JSON_EXISTS(jsonb '[]', '$');
+
+SELECT JSON_EXISTS(jsonb '1', '$.a');
+SELECT JSON_EXISTS(jsonb '1', 'strict $.a'); -- FALSE on error
+SELECT JSON_EXISTS(jsonb '1', 'strict $.a' ERROR ON ERROR);
+SELECT JSON_EXISTS(jsonb 'null', '$.a');
+SELECT JSON_EXISTS(jsonb '[]', '$.a');
+SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'strict $.a'); -- FALSE on error
+SELECT JSON_EXISTS(jsonb '[1, "aaa", {"a": 1}]', 'lax $.a');
+SELECT JSON_EXISTS(jsonb '{}', '$.a');
+SELECT JSON_EXISTS(jsonb '{"b": 1, "a": 2}', '$.a');
+
+SELECT JSON_EXISTS(jsonb '1', '$.a.b');
+SELECT JSON_EXISTS(jsonb '{"a": {"b": 1}}', '$.a.b');
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.a.b');
+
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING 1 AS x);
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x)' PASSING '1' AS x);
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y);
+SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 1 AS y);
+
+-- extension: boolean expressions
+SELECT JSON_EXISTS(jsonb '1', '$ > 2');
+SELECT JSON_EXISTS(jsonb '1', '$.a > 2' ERROR ON ERROR);
+
+-- JSON_VALUE
+
+SELECT JSON_VALUE(NULL::jsonb, '$');
+
+SELECT JSON_VALUE(jsonb 'null', '$');
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING int);
+
+SELECT JSON_VALUE(jsonb 'true', '$');
+SELECT JSON_VALUE(jsonb 'true', '$' RETURNING bool);
+
+SELECT JSON_VALUE(jsonb '123', '$');
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING int) + 234;
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING text);
+/* jsonb bytea ??? */
+SELECT JSON_VALUE(jsonb '123', '$' RETURNING bytea ERROR ON ERROR);
+
+SELECT JSON_VALUE(jsonb '1.23', '$');
+SELECT JSON_VALUE(jsonb '1.23', '$' RETURNING int);
+SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING numeric);
+SELECT JSON_VALUE(jsonb '"1.23"', '$' RETURNING int ERROR ON ERROR);
+
+SELECT JSON_VALUE(jsonb '"aaa"', '$');
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING text);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(5));
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING char(2));
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING json ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING jsonb ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING json);
+SELECT JSON_VALUE(jsonb '"\"aaa\""', '$' RETURNING jsonb);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '"aaa"', '$' RETURNING int DEFAULT 111 ON ERROR);
+SELECT JSON_VALUE(jsonb '"123"', '$' RETURNING int) + 234;
+
+SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9;
+
+-- Test NULL checks execution in domain types
+CREATE DOMAIN sqljsonb_int_not_null AS int NOT NULL;
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null);
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb 'null', '$' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT 2 ON EMPTY ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '1', '$.a' RETURNING sqljsonb_int_not_null DEFAULT NULL ON EMPTY ERROR ON ERROR);
+CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple');
+CREATE DOMAIN rgb AS rainbow CHECK (VALUE IN ('red', 'green', 'blue'));
+SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb);
+SELECT JSON_VALUE('"purple"'::jsonb, 'lax $[*]' RETURNING rgb ERROR ON ERROR);
+
+SELECT JSON_VALUE(jsonb '[]', '$');
+SELECT JSON_VALUE(jsonb '[]', '$' ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '{}', '$');
+SELECT JSON_VALUE(jsonb '{}', '$' ERROR ON ERROR);
+
+SELECT JSON_VALUE(jsonb '1', '$.a');
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 'error' ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'strict $.a' DEFAULT 2 ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT 2 ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' NULL ON EMPTY DEFAULT '2' ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' DEFAULT '2' ON EMPTY DEFAULT '3' ON ERROR);
+SELECT JSON_VALUE(jsonb '1', 'lax $.a' ERROR ON EMPTY DEFAULT '3' ON ERROR);
+
+SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '[1,2]', '$[*]' DEFAULT '0' ON ERROR);
+SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int ERROR ON ERROR);
+SELECT JSON_VALUE(jsonb '[" "]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int DEFAULT 2 + 3 ON ERROR);
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING int FORMAT JSON); -- RETURNING FORMAT not allowed
+
+-- RETUGNING pseudo-types not allowed
+SELECT JSON_VALUE(jsonb '["1"]', '$[*]' RETURNING record);
+
+SELECT
+ x,
+ JSON_VALUE(
+ jsonb '{"a": 1, "b": 2}',
+ '$.* ? (@ > $x)' PASSING x AS x
+ RETURNING int
+ DEFAULT -1 ON EMPTY
+ DEFAULT -2 ON ERROR
+ ) y
+FROM
+ generate_series(0, 2) x;
+
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a);
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point);
+SELECT JSON_VALUE(jsonb 'null', '$a' PASSING point ' (1, 2 )' AS a RETURNING point ERROR ON ERROR);
+
+-- Test PASSING and RETURNING date/time types
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamptz);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING date '2018-02-21 12:34:56 +10' AS ts RETURNING date);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING time '2018-02-21 12:34:56 +10' AS ts RETURNING time);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timetz '2018-02-21 12:34:56 +10' AS ts RETURNING timetz);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamp '2018-02-21 12:34:56 +10' AS ts RETURNING timestamp);
+
+-- Also test RETURNING json[b]
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json);
+SELECT JSON_VALUE(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb);
+
+-- JSON_QUERY
+
+SELECT
+ JSON_QUERY(js, '$'),
+ JSON_QUERY(js, '$' WITHOUT WRAPPER),
+ JSON_QUERY(js, '$' WITH CONDITIONAL WRAPPER),
+ JSON_QUERY(js, '$' WITH UNCONDITIONAL ARRAY WRAPPER),
+ JSON_QUERY(js, '$' WITH ARRAY WRAPPER)
+FROM
+ (VALUES
+ (jsonb 'null'),
+ ('12.3'),
+ ('true'),
+ ('"aaa"'),
+ ('[1, null, "2"]'),
+ ('{"a": 1, "b": [2]}')
+ ) foo(js);
+
+SELECT
+ JSON_QUERY(js, 'strict $[*]') AS "unspec",
+ JSON_QUERY(js, 'strict $[*]' WITHOUT WRAPPER) AS "without",
+ JSON_QUERY(js, 'strict $[*]' WITH CONDITIONAL WRAPPER) AS "with cond",
+ JSON_QUERY(js, 'strict $[*]' WITH UNCONDITIONAL ARRAY WRAPPER) AS "with uncond",
+ JSON_QUERY(js, 'strict $[*]' WITH ARRAY WRAPPER) AS "with"
+FROM
+ (VALUES
+ (jsonb '1'),
+ ('[]'),
+ ('[null]'),
+ ('[12.3]'),
+ ('[true]'),
+ ('["aaa"]'),
+ ('[[1, 2, 3]]'),
+ ('[{"a": 1, "b": [2]}]'),
+ ('[1, "2", null, [3]]')
+ ) foo(js);
+
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text KEEP QUOTES ON SCALAR STRING);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES ON SCALAR STRING);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' OMIT QUOTES ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING json OMIT QUOTES ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING bytea FORMAT JSON OMIT QUOTES ERROR ON ERROR);
+
+-- QUOTES behavior should not be specified when WITH WRAPPER used:
+-- Should fail
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER OMIT QUOTES);
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH WRAPPER KEEP QUOTES);
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER KEEP QUOTES);
+SELECT JSON_QUERY(jsonb '[1]', '$' WITH CONDITIONAL WRAPPER OMIT QUOTES);
+-- Should succeed
+SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER OMIT QUOTES);
+SELECT JSON_QUERY(jsonb '[1]', '$' WITHOUT WRAPPER KEEP QUOTES);
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]');
+SELECT JSON_QUERY(jsonb '[]', '$[*]' NULL ON EMPTY);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ON EMPTY);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY ARRAY ON EMPTY);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' EMPTY OBJECT ON EMPTY);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' DEFAULT '"empty"' ON EMPTY);
+
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY NULL ON ERROR);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY ARRAY ON ERROR);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON EMPTY ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '[]', '$[*]' ERROR ON ERROR);
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' DEFAULT '"empty"' ON ERROR);
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING json FORMAT JSON);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING jsonb FORMAT JSON);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(10));
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING char(3));
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING text FORMAT JSON);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea);
+SELECT JSON_QUERY(jsonb '[1,2]', '$' RETURNING bytea FORMAT JSON);
+
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING bytea FORMAT JSON EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING json EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '[1,2]', '$[*]' RETURNING jsonb EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR);
+SELECT JSON_QUERY(jsonb '"[3,4]"', '$[*]' RETURNING bigint[] EMPTY OBJECT ON ERROR);
+
+-- RETUGNING pseudo-types not allowed
+SELECT JSON_QUERY(jsonb '[3,4]', '$[*]' RETURNING anyarray EMPTY OBJECT ON ERROR);
+
+SELECT
+ x, y,
+ JSON_QUERY(
+ jsonb '[1,2,3,4,5,null]',
+ '$[*] ? (@ >= $x && @ <= $y)'
+ PASSING x AS x, y AS y
+ WITH CONDITIONAL WRAPPER
+ EMPTY ARRAY ON EMPTY
+ ) list
+FROM
+ generate_series(0, 4) x,
+ generate_series(0, 4) y;
+
+-- Extension: record types returning
+CREATE TYPE sqljsonb_rec AS (a int, t text, js json, jb jsonb, jsa json[]);
+CREATE TYPE sqljsonb_reca AS (reca sqljsonb_rec[]);
+
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec);
+SELECT JSON_QUERY(jsonb '[{"a": "a", "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '[{"a": "a", "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING sqljsonb_rec);
+SELECT * FROM unnest((JSON_QUERY(jsonb '{"jsa": [{"a": 1, "b": ["foo"]}, {"a": 2, "c": {}}, 123]}', '$' RETURNING sqljsonb_rec)).jsa);
+SELECT * FROM unnest((JSON_QUERY(jsonb '{"reca": [{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]}', '$' RETURNING sqljsonb_reca)).reca);
+
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath);
+SELECT JSON_QUERY(jsonb '[{"a": 1, "b": "foo", "t": "aaa", "js": [1, "2", {}], "jb": {"x": [1, "2", {}]}}, {"a": 2}]', '$[0]' RETURNING jsonpath ERROR ON ERROR);
+
+-- Extension: array types returning
+SELECT JSON_QUERY(jsonb '[1,2,null,"3"]', '$[*]' RETURNING int[] WITH WRAPPER);
+SELECT JSON_QUERY(jsonb '[1,2,null,"a"]', '$[*]' RETURNING int[] WITH WRAPPER ERROR ON ERROR);
+SELECT JSON_QUERY(jsonb '[1,2,null,"a"]', '$[*]' RETURNING int[] WITH WRAPPER);
+SELECT * FROM unnest(JSON_QUERY(jsonb '[{"a": 1, "t": ["foo", []]}, {"a": 2, "jb": [{}, true]}]', '$' RETURNING sqljsonb_rec[]));
+
+-- Extension: domain types returning
+SELECT JSON_QUERY(jsonb '{"a": 1}', '$.a' RETURNING sqljsonb_int_not_null);
+SELECT JSON_QUERY(jsonb '{"a": 1}', '$.b' RETURNING sqljsonb_int_not_null);
+
+-- Test timestamptz passing and output
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts);
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING json);
+SELECT JSON_QUERY(jsonb 'null', '$ts' PASSING timestamptz '2018-02-21 12:34:56 +10' AS ts RETURNING jsonb);
+
+-- Test constraints
+
+CREATE TABLE test_jsonb_constraints (
+ js text,
+ i int,
+ x jsonb DEFAULT JSON_QUERY(jsonb '[1,2]', '$[*]' WITH WRAPPER)
+ CONSTRAINT test_jsonb_constraint1
+ CHECK (js IS JSON)
+ CONSTRAINT test_jsonb_constraint2
+ CHECK (JSON_EXISTS(js::jsonb, '$.a' PASSING i + 5 AS int, i::text AS txt, array[1,2,3] as arr))
+ CONSTRAINT test_jsonb_constraint3
+ CHECK (JSON_VALUE(js::jsonb, '$.a' RETURNING int DEFAULT '12' ON EMPTY ERROR ON ERROR) > i)
+ CONSTRAINT test_jsonb_constraint4
+ CHECK (JSON_QUERY(js::jsonb, '$.a' WITH CONDITIONAL WRAPPER EMPTY OBJECT ON ERROR) < jsonb '[10]')
+ CONSTRAINT test_jsonb_constraint5
+ CHECK (JSON_QUERY(js::jsonb, '$.a' RETURNING char(5) OMIT QUOTES EMPTY ARRAY ON EMPTY) > 'a' COLLATE "C")
+);
+
+\d test_jsonb_constraints
+
+SELECT check_clause
+FROM information_schema.check_constraints
+WHERE constraint_name LIKE 'test_jsonb_constraint%'
+ORDER BY 1;
+
+SELECT pg_get_expr(adbin, adrelid)
+FROM pg_attrdef
+WHERE adrelid = 'test_jsonb_constraints'::regclass
+ORDER BY 1;
+
+SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'test_jsonb_constraints'::regclass;
+
+INSERT INTO test_jsonb_constraints VALUES ('', 1);
+INSERT INTO test_jsonb_constraints VALUES ('1', 1);
+INSERT INTO test_jsonb_constraints VALUES ('[]');
+INSERT INTO test_jsonb_constraints VALUES ('{"b": 1}', 1);
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 1}', 1);
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 7}', 1);
+INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1);
+
+DROP TABLE test_jsonb_constraints;
+
+-- Test mutabilily od query functions
+CREATE TABLE test_jsonb_mutability(js jsonb);
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))'));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x));
+CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x));
+DROP TABLE test_jsonb_mutability;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d659adbfd6..9ee58ec2b0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1247,6 +1247,7 @@ Join
JoinCostWorkspace
JoinDomain
JoinExpr
+JsonFuncExpr
JoinHashEntry
JoinPath
JoinPathExtraData
@@ -1257,18 +1258,29 @@ JsObject
JsValue
JsonAggConstructor
JsonAggState
+JsonArgument
JsonArrayAgg
JsonArrayConstructor
JsonArrayQueryConstructor
JsonBaseObjectInfo
+JsonBehavior
+JsonBehaviorType
+JsonCoercion
+JsonCoercionState
JsonConstructorExpr
JsonConstructorExprState
JsonConstructorType
JsonEncoding
+JsonExpr
+JsonExprOp
+JsonExprPostEvalState
+JsonExprState
+JsonCoercionState
JsonFormat
JsonFormatType
JsonHashEntry
JsonIsPredicate
+JsonItemType
JsonIterateStringValuesAction
JsonKeyValue
JsonLexContext
@@ -1286,6 +1298,7 @@ JsonParseContext
JsonParseErrorType
JsonPath
JsonPathBool
+JsonPathDatatypeStatus
JsonPathExecContext
JsonPathExecResult
JsonPathGinAddPathItemFunc
@@ -1298,10 +1311,15 @@ JsonPathGinPathItem
JsonPathItem
JsonPathItemType
JsonPathKeyword
+JsonPathMutableContext
JsonPathParseItem
JsonPathParseResult
JsonPathPredicateCallback
JsonPathString
+JsonPathVarCallback
+JsonPathVariable
+JsonPathVariableEvalContext
+JsonQuotes
JsonReturning
JsonScalarExpr
JsonSemAction
@@ -1318,6 +1336,7 @@ JsonValueExpr
JsonValueList
JsonValueListIterator
JsonValueType
+JsonWrapper
Jsonb
JsonbAggState
JsonbContainer
--
2.35.3
[application/octet-stream] v28-0001-Add-soft-error-handling-to-some-expression-nodes.patch (9.5K, ../../CA+HiwqET8gAhoCqNx_ueELrs4UevhRMNOYSmq=WykOu9THn=og@mail.gmail.com/7-v28-0001-Add-soft-error-handling-to-some-expression-nodes.patch)
download | inline diff:
From 0a4bc2d5ccedf652cd538e45a5f82035165bc5fd Mon Sep 17 00:00:00 2001
From: Amit Langote <[email protected]>
Date: Wed, 22 Nov 2023 13:18:39 +0900
Subject: [PATCH v28 1/6] Add soft error handling to some expression nodes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This adjusts the CoerceViaIO and CoerceToDomain expression evaluation
code to handle errors softly.
For CoerceViaIo, this adds a new ExprEvalStep opcode
EEOP_IOCOERCE_SAFE, which is implemented in new function
ExecEvalCoerceViaIOSafe(). The only difference from EEOP_IOCOERCE's
inline implementation is that the input function receives an
ErrorSaveContext via the function's FunctionCallInfo.context, which
it can use to handle errors softly.
For CoerceToDomain, this simply entails replacing the ereport() in
ExecEvalConstraintNotNull() and ExecEvalConstraintCheck() by
errsave() passing it the ErrorSaveContext passed in the expression's
ExprEvalStep.
In both cases, the ErrorSaveContext to be used is passed by setting
ExprState.escontext to point to it before calling ExecInitExprRec()
on the expression tree whose errors are to be suppressed.
Note that no call site of ExecInitExprRec() has been changed in this
commit, so there's no functional change. This is intended for
implementing new SQL/JSON expression nodes in future commits that
will use to it suppress errors that may occur during type coercions.
Reviewed-by: Álvaro Herrera
Reviewed-by: Andres Freund
Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com
---
src/backend/executor/execExpr.c | 8 ++-
src/backend/executor/execExprInterp.c | 74 ++++++++++++++++++++++++++-
src/backend/jit/llvm/llvmjit_expr.c | 6 +++
src/backend/jit/llvm/llvmjit_types.c | 1 +
src/include/executor/execExpr.h | 4 ++
src/include/nodes/execnodes.h | 7 +++
6 files changed, 97 insertions(+), 3 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 2c62b0c9c8..34bd2102b5 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1563,7 +1563,10 @@ ExecInitExprRec(Expr *node, ExprState *state,
* We don't check permissions here as a type's input/output
* function are assumed to be executable by everyone.
*/
- scratch.opcode = EEOP_IOCOERCE;
+ if (state->escontext == NULL)
+ scratch.opcode = EEOP_IOCOERCE;
+ else
+ scratch.opcode = EEOP_IOCOERCE_SAFE;
/* lookup the source type's output function */
scratch.d.iocoerce.finfo_out = palloc0(sizeof(FmgrInfo));
@@ -1599,6 +1602,8 @@ ExecInitExprRec(Expr *node, ExprState *state,
fcinfo_in->args[2].value = Int32GetDatum(-1);
fcinfo_in->args[2].isnull = false;
+ fcinfo_in->context = (Node *) state->escontext;
+
ExprEvalPushStep(state, &scratch);
break;
}
@@ -3306,6 +3311,7 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest,
/* we'll allocate workspace only if needed */
scratch->d.domaincheck.checkvalue = NULL;
scratch->d.domaincheck.checknull = NULL;
+ scratch->d.domaincheck.escontext = state->escontext;
/*
* Evaluate argument - it's fine to directly store it into resv/resnull,
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 24c2b60c62..d5db96444c 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -63,6 +63,7 @@
#include "executor/nodeSubplan.h"
#include "funcapi.h"
#include "miscadmin.h"
+#include "nodes/miscnodes.h"
#include "nodes/nodeFuncs.h"
#include "parser/parsetree.h"
#include "pgstat.h"
@@ -452,6 +453,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
&&CASE_EEOP_CASE_TESTVAL,
&&CASE_EEOP_MAKE_READONLY,
&&CASE_EEOP_IOCOERCE,
+ &&CASE_EEOP_IOCOERCE_SAFE,
&&CASE_EEOP_DISTINCT,
&&CASE_EEOP_NOT_DISTINCT,
&&CASE_EEOP_NULLIF,
@@ -1205,6 +1207,12 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
EEO_NEXT();
}
+ EEO_CASE(EEOP_IOCOERCE_SAFE)
+ {
+ ExecEvalCoerceViaIOSafe(state, op);
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_DISTINCT)
{
/*
@@ -2510,6 +2518,68 @@ ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
errmsg("no value found for parameter %d", paramId)));
}
+/*
+ * Evaluate a CoerceViaIO node in soft-error mode.
+ *
+ * The source value is in op's result variable.
+ */
+void
+ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op)
+{
+ char *str;
+
+ /* call output function (similar to OutputFunctionCall) */
+ if (*op->resnull)
+ {
+ /* output functions are not called on nulls */
+ str = NULL;
+ }
+ else
+ {
+ FunctionCallInfo fcinfo_out;
+
+ fcinfo_out = op->d.iocoerce.fcinfo_data_out;
+ fcinfo_out->args[0].value = *op->resvalue;
+ fcinfo_out->args[0].isnull = false;
+
+ fcinfo_out->isnull = false;
+ str = DatumGetCString(FunctionCallInvoke(fcinfo_out));
+
+ /* OutputFunctionCall assumes result isn't null */
+ Assert(!fcinfo_out->isnull);
+ }
+
+ /* call input function (similar to InputFunctionCall) */
+ if (!op->d.iocoerce.finfo_in->fn_strict || str != NULL)
+ {
+ FunctionCallInfo fcinfo_in;
+
+ fcinfo_in = op->d.iocoerce.fcinfo_data_in;
+ fcinfo_in->args[0].value = PointerGetDatum(str);
+ fcinfo_in->args[0].isnull = *op->resnull;
+ /* second and third arguments are already set up */
+
+ /* ErrorSaveContext must be present. */
+ Assert(IsA(fcinfo_in->context, ErrorSaveContext));
+
+ fcinfo_in->isnull = false;
+ *op->resvalue = FunctionCallInvoke(fcinfo_in);
+
+ if (SOFT_ERROR_OCCURRED(fcinfo_in->context))
+ {
+ *op->resnull = true;
+ *op->resvalue = (Datum) 0;
+ return;
+ }
+
+ /* Should get null result if and only if str is NULL */
+ if (str == NULL)
+ Assert(*op->resnull);
+ else
+ Assert(!*op->resnull);
+ }
+}
+
/*
* Evaluate a SQLValueFunction expression.
*/
@@ -3730,7 +3800,7 @@ void
ExecEvalConstraintNotNull(ExprState *state, ExprEvalStep *op)
{
if (*op->resnull)
- ereport(ERROR,
+ errsave((Node *) op->d.domaincheck.escontext,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("domain %s does not allow null values",
format_type_be(op->d.domaincheck.resulttype)),
@@ -3745,7 +3815,7 @@ ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op)
{
if (!*op->d.domaincheck.checknull &&
!DatumGetBool(*op->d.domaincheck.checkvalue))
- ereport(ERROR,
+ errsave((Node *) op->d.domaincheck.escontext,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("value for domain %s violates check constraint \"%s\"",
format_type_be(op->d.domaincheck.resulttype),
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index a3a0876bff..81856a9dc7 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1431,6 +1431,12 @@ llvm_compile_expr(ExprState *state)
break;
}
+ case EEOP_IOCOERCE_SAFE:
+ build_EvalXFunc(b, mod, "ExecEvalCoerceViaIOSafe",
+ v_state, op);
+ LLVMBuildBr(b, opblocks[opno + 1]);
+ break;
+
case EEOP_DISTINCT:
case EEOP_NOT_DISTINCT:
{
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
index 791902ff1f..3a4be09e50 100644
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -162,6 +162,7 @@ void *referenced_functions[] =
ExecEvalRow,
ExecEvalRowNotNull,
ExecEvalRowNull,
+ ExecEvalCoerceViaIOSafe,
ExecEvalSQLValueFunction,
ExecEvalScalarArrayOp,
ExecEvalHashedScalarArrayOp,
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 048573c2bc..c4fd933154 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -16,6 +16,7 @@
#include "executor/nodeAgg.h"
#include "nodes/execnodes.h"
+#include "nodes/miscnodes.h"
/* forward references to avoid circularity */
struct ExprEvalStep;
@@ -168,6 +169,7 @@ typedef enum ExprEvalOp
/* evaluate assorted special-purpose expression types */
EEOP_IOCOERCE,
+ EEOP_IOCOERCE_SAFE,
EEOP_DISTINCT,
EEOP_NOT_DISTINCT,
EEOP_NULLIF,
@@ -547,6 +549,7 @@ typedef struct ExprEvalStep
bool *checknull;
/* OID of domain type */
Oid resulttype;
+ ErrorSaveContext *escontext;
} domaincheck;
/* for EEOP_CONVERT_ROWTYPE */
@@ -776,6 +779,7 @@ extern void ExecEvalParamExec(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
+extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op);
extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op);
extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op);
extern void ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 5d7f17dee0..6a7118d300 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -34,6 +34,7 @@
#include "fmgr.h"
#include "lib/ilist.h"
#include "lib/pairingheap.h"
+#include "nodes/miscnodes.h"
#include "nodes/params.h"
#include "nodes/plannodes.h"
#include "nodes/tidbitmap.h"
@@ -129,6 +130,12 @@ typedef struct ExprState
Datum *innermost_domainval;
bool *innermost_domainnull;
+
+ /*
+ * For expression nodes that support soft errors. Should be set to NULL
+ * before calling ExecInitExprRec() if the caller wants errors thrown.
+ */
+ ErrorSaveContext *escontext;
} ExprState;
--
2.35.3
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
@ 2023-12-07 03:10 ` jian he <[email protected]>
2023-12-07 10:39 ` Re: remaining sql/json patches jian he <[email protected]>
1 sibling, 1 reply; 28+ messages in thread
From: jian he @ 2023-12-07 03:10 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
On Wed, Dec 6, 2023 at 10:02 PM Amit Langote <[email protected]> wrote:
>
> Finally, I also fixed a couple of silly mistakes in 0003 around
> transformJsonBehavior() and some further assorted tightening in the ON
> ERROR / EMPTY expression coercion handling code.
>
typo:
+ * If a soft-error occurs, it will be checked by EEOP_JSONEXPR_COECION_FINISH
json_exists no RETURNING clause.
so the following part in src/backend/parser/parse_expr.c can be removed?
+ else if (jsexpr->returning->typid != BOOLOID)
+ {
+ Node *coercion_expr;
+ CaseTestExpr *placeholder = makeNode(CaseTestExpr);
+ int location = exprLocation((Node *) jsexpr);
+
+ /*
+ * We abuse CaseTestExpr here as placeholder to pass the
+ * result of evaluating JSON_EXISTS to the coercion
+ * expression.
+ */
+ placeholder->typeId = BOOLOID;
+ placeholder->typeMod = -1;
+ placeholder->collation = InvalidOid;
+
+ coercion_expr =
+ coerce_to_target_type(pstate, (Node *) placeholder, BOOLOID,
+ jsexpr->returning->typid,
+ jsexpr->returning->typmod,
+ COERCION_EXPLICIT,
+ COERCE_IMPLICIT_CAST,
+ location);
+
+ if (coercion_expr == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CANNOT_COERCE),
+ errmsg("cannot cast type %s to %s",
+ format_type_be(BOOLOID),
+ format_type_be(jsexpr->returning->typid)),
+ parser_coercion_errposition(pstate, location, (Node *) jsexpr)));
+
+ if (coercion_expr != (Node *) placeholder)
+ jsexpr->result_coercion = coercion_expr;
+ }
Similarly, since JSON_EXISTS has no RETURNING clause, the following
also needs to be refactored?
+ /*
+ * Disallow FORMAT specification in the RETURNING clause of JSON_EXISTS()
+ * and JSON_VALUE().
+ */
+ if (func->output &&
+ (func->op == JSON_VALUE_OP || func->op == JSON_EXISTS_OP))
+ {
+ JsonFormat *format = func->output->returning->format;
+
+ if (format->format_type != JS_FORMAT_DEFAULT ||
+ format->encoding != JS_ENC_DEFAULT)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("cannot specify FORMAT in RETURNING clause of %s",
+ func->op == JSON_VALUE_OP ? "JSON_VALUE()" :
+ "JSON_EXISTS()"),
+ parser_errposition(pstate, format->location)));
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
2023-12-07 03:10 ` Re: remaining sql/json patches jian he <[email protected]>
@ 2023-12-07 10:39 ` jian he <[email protected]>
2023-12-07 12:13 ` Re: remaining sql/json patches Amit Langote <[email protected]>
0 siblings, 1 reply; 28+ messages in thread
From: jian he @ 2023-12-07 10:39 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
two JsonCoercionState in src/tools/pgindent/typedefs.list.
+JsonCoercionState
JsonConstructorExpr
JsonConstructorExprState
JsonConstructorType
JsonEncoding
+JsonExpr
+JsonExprOp
+JsonExprPostEvalState
+JsonExprState
+JsonCoercionState
+ post_eval->jump_eval_coercion = jsestate->jump_eval_result_coercion;
+ if (jbv == NULL)
+ {
+ /* Will be coerced with result_coercion. */
+ *op->resvalue = (Datum) 0;
+ *op->resnull = true;
+ }
+ else if (!error && !empty)
+ {
+ Assert(jbv != NULL);
the above "Assert(jbv != NULL);" will always be true?
based on:
json_behavior_clause_opt:
json_behavior ON EMPTY_P
{ $$ = list_make2($1, NULL); }
| json_behavior ON ERROR_P
{ $$ = list_make2(NULL, $1); }
| json_behavior ON EMPTY_P json_behavior ON ERROR_P
{ $$ = list_make2($1, $4); }
| /* EMPTY */
{ $$ = list_make2(NULL, NULL); }
;
so
+ if (func->behavior)
+ {
+ on_empty = linitial(func->behavior);
+ on_error = lsecond(func->behavior);
+ }
`if (func->behavior)` will always be true?
By the way, in the above "{ $$ = list_make2($1, $4); }" what does $4
refer to? (I don't know gram.y....)
+ jsexpr->formatted_expr = transformJsonValueExpr(pstate, constructName,
+ func->context_item,
+ JS_FORMAT_JSON,
+ InvalidOid, false);
+
+ Assert(jsexpr->formatted_expr != NULL);
This Assert is unnecessary? transformJsonValueExpr function already
has an assert in the end, will it fail that one first?
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
2023-12-07 03:10 ` Re: remaining sql/json patches jian he <[email protected]>
2023-12-07 10:39 ` Re: remaining sql/json patches jian he <[email protected]>
@ 2023-12-07 12:13 ` Amit Langote <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Amit Langote @ 2023-12-07 12:13 UTC (permalink / raw)
To: jian he <[email protected]>; +Cc: Alvaro Herrera <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
On Thu, Dec 7, 2023 at 7:39 PM jian he <[email protected]> wrote:
> based on:
> json_behavior_clause_opt:
> json_behavior ON EMPTY_P
> { $$ = list_make2($1, NULL); }
> | json_behavior ON ERROR_P
> { $$ = list_make2(NULL, $1); }
> | json_behavior ON EMPTY_P json_behavior ON ERROR_P
> { $$ = list_make2($1, $4); }
> | /* EMPTY */
> { $$ = list_make2(NULL, NULL); }
> ;
> so
> + if (func->behavior)
> + {
> + on_empty = linitial(func->behavior);
> + on_error = lsecond(func->behavior);
> + }
Yeah, maybe.
> `if (func->behavior)` will always be true?
> By the way, in the above "{ $$ = list_make2($1, $4); }" what does $4
> refer to? (I don't know gram.y....)
$1 and $4 refer to the 1st and 4th symbols in the following:
json_behavior ON EMPTY_P json_behavior ON ERROR_P
So $1 gives the json_behavior (JsonBehavior) node for ON EMPTY and $4
gives that for ON ERROR.
--
Thanks, Amit Langote
EDB: http://www.enterprisedb.com
^ permalink raw reply [nested|flat] 28+ messages in thread
* Re: remaining sql/json patches
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
@ 2023-12-07 08:25 ` Peter Eisentraut <[email protected]>
1 sibling, 0 replies; 28+ messages in thread
From: Peter Eisentraut @ 2023-12-07 08:25 UTC (permalink / raw)
To: Amit Langote <[email protected]>; Alvaro Herrera <[email protected]>; +Cc: jian he <[email protected]>; Andres Freund <[email protected]>; Andrew Dunstan <[email protected]>; Erik Rijkers <[email protected]>; pgsql-hackers
Here are a couple of small patches to tidy up the parser a bit in your
v28-0004 (JSON_TABLE) patch. It's not a lot; the rest looks okay to me.
(I don't have an opinion on the concurrent discussion on resolving
some precedence issues.)
From 0dc7e7852702272f0bf12aaa4b56b9ac60c4d969 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 08:56:26 +0100
Subject: [PATCH 1/3] Fix spurious tab characters
---
src/backend/parser/gram.y | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3755434af0..78aaa7a32f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16661,7 +16661,7 @@ json_quotes_clause_opt:
json_table:
JSON_TABLE '('
json_value_expr ',' a_expr json_passing_clause_opt
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
json_table_plan_clause_opt
json_behavior_clause_opt
')'
@@ -16680,7 +16680,7 @@ json_table:
}
| JSON_TABLE '('
json_value_expr ',' a_expr AS name json_passing_clause_opt
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
json_table_plan_clause_opt
json_behavior_clause_opt
')'
@@ -16772,7 +16772,7 @@ json_table_column_definition:
$$ = (Node *) n;
}
| NESTED path_opt Sconst
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16784,7 +16784,7 @@ json_table_column_definition:
$$ = (Node *) n;
}
| NESTED path_opt Sconst AS name
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
--
2.43.0
From 5ee8a90940d107fc3bf93cccfde2e6a511218377 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 09:15:21 +0100
Subject: [PATCH 2/3] Light reformatting
---
src/backend/parser/gram.y | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 78aaa7a32f..4fad661ff0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16883,12 +16883,14 @@ json_table_plan_sibling:
;
json_table_default_plan_choices:
- json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; }
- | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_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
+ { $$ = $1 | JSTPJ_UNION; }
+ | json_table_default_plan_union_cross
+ { $$ = $1 | JSTPJ_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:
--
2.43.0
From a57cea1ea87fe78e6b4e90cef6bccccbecde67fa Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 09:15:36 +0100
Subject: [PATCH 3/3] Remove some unnecessary intermediate rules
---
src/backend/parser/gram.y | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4fad661ff0..b5eb73acf9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -657,10 +657,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
json_table_plan_clause_opt
json_table_plan
json_table_plan_simple
- json_table_plan_parent_child
json_table_plan_outer
json_table_plan_inner
- json_table_plan_sibling
json_table_plan_union
json_table_plan_cross
json_table_plan_primary
@@ -16823,8 +16821,10 @@ json_table_plan_clause_opt:
json_table_plan:
json_table_plan_simple
- | json_table_plan_parent_child
- | json_table_plan_sibling
+ | json_table_plan_outer
+ | json_table_plan_inner
+ | json_table_plan_union
+ | json_table_plan_cross
;
json_table_plan_simple:
@@ -16858,11 +16858,6 @@ json_table_plan_inner:
{ $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); }
;
-json_table_plan_parent_child:
- json_table_plan_outer
- | json_table_plan_inner
- ;
-
json_table_plan_union:
json_table_plan_primary UNION json_table_plan_primary
{ $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); }
@@ -16877,11 +16872,6 @@ json_table_plan_cross:
{ $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); }
;
-json_table_plan_sibling:
- json_table_plan_union
- | json_table_plan_cross
- ;
-
json_table_default_plan_choices:
json_table_default_plan_inner_outer
{ $$ = $1 | JSTPJ_UNION; }
--
2.43.0
Attachments:
[text/plain] 0001-Fix-spurious-tab-characters.patch.nocfbot (1.6K, ../../[email protected]/2-0001-Fix-spurious-tab-characters.patch.nocfbot)
download | inline diff:
From 0dc7e7852702272f0bf12aaa4b56b9ac60c4d969 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 08:56:26 +0100
Subject: [PATCH 1/3] Fix spurious tab characters
---
src/backend/parser/gram.y | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3755434af0..78aaa7a32f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16661,7 +16661,7 @@ json_quotes_clause_opt:
json_table:
JSON_TABLE '('
json_value_expr ',' a_expr json_passing_clause_opt
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
json_table_plan_clause_opt
json_behavior_clause_opt
')'
@@ -16680,7 +16680,7 @@ json_table:
}
| JSON_TABLE '('
json_value_expr ',' a_expr AS name json_passing_clause_opt
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
json_table_plan_clause_opt
json_behavior_clause_opt
')'
@@ -16772,7 +16772,7 @@ json_table_column_definition:
$$ = (Node *) n;
}
| NESTED path_opt Sconst
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
@@ -16784,7 +16784,7 @@ json_table_column_definition:
$$ = (Node *) n;
}
| NESTED path_opt Sconst AS name
- COLUMNS '(' json_table_column_definition_list ')'
+ COLUMNS '(' json_table_column_definition_list ')'
{
JsonTableColumn *n = makeNode(JsonTableColumn);
--
2.43.0
[text/plain] 0002-Light-reformatting.patch.nocfbot (1.3K, ../../[email protected]/3-0002-Light-reformatting.patch.nocfbot)
download | inline diff:
From 5ee8a90940d107fc3bf93cccfde2e6a511218377 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 09:15:21 +0100
Subject: [PATCH 2/3] Light reformatting
---
src/backend/parser/gram.y | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 78aaa7a32f..4fad661ff0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -16883,12 +16883,14 @@ json_table_plan_sibling:
;
json_table_default_plan_choices:
- json_table_default_plan_inner_outer { $$ = $1 | JSTPJ_UNION; }
- | json_table_default_plan_union_cross { $$ = $1 | JSTPJ_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
+ { $$ = $1 | JSTPJ_UNION; }
+ | json_table_default_plan_union_cross
+ { $$ = $1 | JSTPJ_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:
--
2.43.0
[text/plain] 0003-Remove-some-unnecessary-intermediate-rules.patch.nocfbot (1.8K, ../../[email protected]/4-0003-Remove-some-unnecessary-intermediate-rules.patch.nocfbot)
download | inline diff:
From a57cea1ea87fe78e6b4e90cef6bccccbecde67fa Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Thu, 7 Dec 2023 09:15:36 +0100
Subject: [PATCH 3/3] Remove some unnecessary intermediate rules
---
src/backend/parser/gram.y | 18 ++++--------------
1 file changed, 4 insertions(+), 14 deletions(-)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 4fad661ff0..b5eb73acf9 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -657,10 +657,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
json_table_plan_clause_opt
json_table_plan
json_table_plan_simple
- json_table_plan_parent_child
json_table_plan_outer
json_table_plan_inner
- json_table_plan_sibling
json_table_plan_union
json_table_plan_cross
json_table_plan_primary
@@ -16823,8 +16821,10 @@ json_table_plan_clause_opt:
json_table_plan:
json_table_plan_simple
- | json_table_plan_parent_child
- | json_table_plan_sibling
+ | json_table_plan_outer
+ | json_table_plan_inner
+ | json_table_plan_union
+ | json_table_plan_cross
;
json_table_plan_simple:
@@ -16858,11 +16858,6 @@ json_table_plan_inner:
{ $$ = makeJsonTableJoinedPlan(JSTPJ_INNER, $1, $3, @1); }
;
-json_table_plan_parent_child:
- json_table_plan_outer
- | json_table_plan_inner
- ;
-
json_table_plan_union:
json_table_plan_primary UNION json_table_plan_primary
{ $$ = makeJsonTableJoinedPlan(JSTPJ_UNION, $1, $3, @1); }
@@ -16877,11 +16872,6 @@ json_table_plan_cross:
{ $$ = makeJsonTableJoinedPlan(JSTPJ_CROSS, $1, $3, @1); }
;
-json_table_plan_sibling:
- json_table_plan_union
- | json_table_plan_cross
- ;
-
json_table_default_plan_choices:
json_table_default_plan_inner_outer
{ $$ = $1 | JSTPJ_UNION; }
--
2.43.0
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/5] Use background worker to do logical decoding.
@ 2025-12-09 18:44 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2025-12-09 18:44 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1139 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 68 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 932 insertions(+), 410 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 01be29eb405..e6d630fa2f7 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 501bd36c23e..f2a2ec6d3e5 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -46,6 +46,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -56,6 +58,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -123,6 +127,103 @@ typedef struct ChangeDest
IndexInsertState *iistate;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Can backend read the file contents? */
+ bool sfs_valid;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate output file name. If relations of the same 'relid' happen to be
+ * processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -130,7 +231,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -143,12 +244,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -160,9 +259,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -172,7 +271,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -182,6 +280,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -604,20 +709,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -914,7 +1019,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -928,19 +1032,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
+ *
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
+ */
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -965,7 +1086,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -989,15 +1110,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
PopActiveSnapshot();
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1168,8 +1285,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1330,7 +1446,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2363,59 +2478,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* This function is much like pg_create_logical_replication_slot() except that
* the new slot is neither released (if anyone else could read changes from
@@ -2427,9 +2489,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2437,21 +2500,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2473,197 +2536,238 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
+ RepackDecodingState *dstate;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
+ dstate = (RepackDecodingState *) ctx->output_writer_private;
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
- return result;
-}
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
-{
- RepackDecodingState *dstate;
- ResourceOwner resowner_old;
-
- /*
- * Invalidate the "present" cache before moving to "(recent) history".
- */
- InvalidateSystemCaches();
+ while (XLogRecPtrIsInvalid(lsn_upto) || ctx->reader->EndRecPtr < lsn_upto)
+ {
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
+ CHECK_FOR_INTERRUPTS();
- PG_TRY();
- {
- while (ctx->reader->EndRecPtr < end_of_wal)
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record == NULL)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ ReadLocalXLogPageNoWaitPrivate *priv;
- record = XLogReadRecord(ctx->reader, &errm);
if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ ereport(ERROR, (errmsg("%s", errm)));
/*
- * If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible. The point is that the worker is only useful if
+ * it starts decoding before lsn_upto is set. Thus it can reach
+ * the end of WAL and find out later that it did not have to go
+ * that far.
*/
- end_lsn = ctx->reader->EndRecPtr;
- XLByteToSeg(end_lsn, segno_new, wal_segment_size);
- if (segno_new != repack_current_segment)
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
{
- LogicalConfirmReceivedLocation(end_lsn);
- elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
- (uint32) (end_lsn >> 32), (uint32) end_lsn);
- repack_current_segment = segno_new;
+ priv->end_of_wal = false;
+
+ /* Do we know how far we should get? */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ /* Check if the work happens to be complete. */
+ continue;
+ }
+
+ /* Wait a bit before we retry reading WAL. */
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 1000L,
+ WAIT_EVENT_REPACK_WORKER_MAIN);
+
+ continue;
}
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
- CHECK_FOR_INTERRUPTS();
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
+
+ /*
+ * If WAL segment boundary has been crossed, inform the decoding
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size seems
+ * appropriate for restart_lsn (because less than a segment cannot be
+ * recycled anyway), however more frequent checks might be beneficial
+ * for catalog_xmin.
+ */
+ end_lsn = ctx->reader->EndRecPtr;
+ XLByteToSeg(end_lsn, segno_new, wal_segment_size);
+ if (segno_new != repack_current_segment)
+ {
+ LogicalConfirmReceivedLocation(end_lsn);
+ elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
+ (uint32) (end_lsn >> 32), (uint32) end_lsn);
+ repack_current_segment = segno_new;
+ }
+
+ /* Keep checking if 'lsn_upto' was specified. */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file and make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
- * Apply changes that happened during the initial load.
- *
- * Scan key is passed by caller, so it does not have to be constructed
- * multiple times. Key entries have all fields initialized, except for
- * sk_argument.
+ * Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2671,12 +2775,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2693,7 +2796,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2708,26 +2811,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2865,6 +2961,12 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
/* XXX no instrumentation for now */
scan = index_beginscan(rel, ident_index, GetActiveSnapshot(),
NULL, dest->ident_key_nentries, 0);
+
+ /*
+ * Scan key is passed by caller, so it does not have to be constructed
+ * multiple times. Key entries have all fields initialized, except for
+ * sk_argument.
+ */
index_rescan(scan, dest->ident_key, dest->ident_key_nentries, NULL, 0);
/* Info needed to retrieve key values from heap tuple. */
@@ -2900,25 +3002,58 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ /* No file is exported until the worker exports the next one. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ BufFileClose(file);
}
/*
@@ -3044,15 +3179,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3067,7 +3197,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3170,7 +3299,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3266,8 +3395,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3380,3 +3512,488 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->sfs_valid = false;
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ Assert(!shared->sfs_valid);
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * The worker already had to access some system catalogs during startup,
+ * and we even had to open the relation we are processing. Now that we're
+ * going to work with historic snapshots, the system caches must be
+ * invalidated.
+ */
+ InvalidateSystemCaches();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
+
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessParallelMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res = SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 2b75de0ddef..28a5a400fb1 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 142a02eb5e9..b368990e90b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -135,6 +136,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 866f92cf799..85323ba61e2 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -205,7 +205,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 087821311cc..af12144795b 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -694,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..4a4858882f0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f39830dbb34..cbcc8550960 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -153,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index d8f76d325f9..2b983abce3e 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1652,7 +1650,7 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
* - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
+ * changes. NULL if background worker takes care of the decoding.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1664,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1672,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index b43a1740053..0ac70ec30d7 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,6 +17,7 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
#include "storage/relfilelocator.h"
#include "utils/relcache.h"
@@ -47,6 +48,9 @@ typedef struct ClusterParams
extern RelFileLocator repacked_rel_locator;
extern RelFileLocator repacked_rel_toast_locator;
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -55,68 +59,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -125,9 +91,6 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
LOCKMODE lockmode);
extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
-
extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
char relpersistence, LOCKMODE lockmode);
extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
@@ -140,4 +103,5 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
MultiXactId cutoffMulti,
char newrelpersistence);
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index afeeb1ca019..c0a66516b66 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3139b14e85f..35344910f65 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -488,7 +488,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -629,6 +628,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/5] Use background worker to do logical decoding.
@ 2025-12-09 18:44 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2025-12-09 18:44 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1139 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 68 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 932 insertions(+), 410 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 01be29eb405..e6d630fa2f7 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 501bd36c23e..f2a2ec6d3e5 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -46,6 +46,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -56,6 +58,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -123,6 +127,103 @@ typedef struct ChangeDest
IndexInsertState *iistate;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Can backend read the file contents? */
+ bool sfs_valid;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate output file name. If relations of the same 'relid' happen to be
+ * processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -130,7 +231,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -143,12 +244,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -160,9 +259,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -172,7 +271,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -182,6 +280,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -604,20 +709,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -914,7 +1019,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -928,19 +1032,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
+ *
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
+ */
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -965,7 +1086,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -989,15 +1110,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
PopActiveSnapshot();
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1168,8 +1285,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1330,7 +1446,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2363,59 +2478,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* This function is much like pg_create_logical_replication_slot() except that
* the new slot is neither released (if anyone else could read changes from
@@ -2427,9 +2489,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2437,21 +2500,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2473,197 +2536,238 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
+ RepackDecodingState *dstate;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
+ dstate = (RepackDecodingState *) ctx->output_writer_private;
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
- return result;
-}
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
-{
- RepackDecodingState *dstate;
- ResourceOwner resowner_old;
-
- /*
- * Invalidate the "present" cache before moving to "(recent) history".
- */
- InvalidateSystemCaches();
+ while (XLogRecPtrIsInvalid(lsn_upto) || ctx->reader->EndRecPtr < lsn_upto)
+ {
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
+ CHECK_FOR_INTERRUPTS();
- PG_TRY();
- {
- while (ctx->reader->EndRecPtr < end_of_wal)
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record == NULL)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ ReadLocalXLogPageNoWaitPrivate *priv;
- record = XLogReadRecord(ctx->reader, &errm);
if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ ereport(ERROR, (errmsg("%s", errm)));
/*
- * If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible. The point is that the worker is only useful if
+ * it starts decoding before lsn_upto is set. Thus it can reach
+ * the end of WAL and find out later that it did not have to go
+ * that far.
*/
- end_lsn = ctx->reader->EndRecPtr;
- XLByteToSeg(end_lsn, segno_new, wal_segment_size);
- if (segno_new != repack_current_segment)
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
{
- LogicalConfirmReceivedLocation(end_lsn);
- elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
- (uint32) (end_lsn >> 32), (uint32) end_lsn);
- repack_current_segment = segno_new;
+ priv->end_of_wal = false;
+
+ /* Do we know how far we should get? */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ /* Check if the work happens to be complete. */
+ continue;
+ }
+
+ /* Wait a bit before we retry reading WAL. */
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 1000L,
+ WAIT_EVENT_REPACK_WORKER_MAIN);
+
+ continue;
}
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
- CHECK_FOR_INTERRUPTS();
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
+
+ /*
+ * If WAL segment boundary has been crossed, inform the decoding
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size seems
+ * appropriate for restart_lsn (because less than a segment cannot be
+ * recycled anyway), however more frequent checks might be beneficial
+ * for catalog_xmin.
+ */
+ end_lsn = ctx->reader->EndRecPtr;
+ XLByteToSeg(end_lsn, segno_new, wal_segment_size);
+ if (segno_new != repack_current_segment)
+ {
+ LogicalConfirmReceivedLocation(end_lsn);
+ elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
+ (uint32) (end_lsn >> 32), (uint32) end_lsn);
+ repack_current_segment = segno_new;
+ }
+
+ /* Keep checking if 'lsn_upto' was specified. */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file and make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
- * Apply changes that happened during the initial load.
- *
- * Scan key is passed by caller, so it does not have to be constructed
- * multiple times. Key entries have all fields initialized, except for
- * sk_argument.
+ * Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2671,12 +2775,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2693,7 +2796,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2708,26 +2811,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2865,6 +2961,12 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
/* XXX no instrumentation for now */
scan = index_beginscan(rel, ident_index, GetActiveSnapshot(),
NULL, dest->ident_key_nentries, 0);
+
+ /*
+ * Scan key is passed by caller, so it does not have to be constructed
+ * multiple times. Key entries have all fields initialized, except for
+ * sk_argument.
+ */
index_rescan(scan, dest->ident_key, dest->ident_key_nentries, NULL, 0);
/* Info needed to retrieve key values from heap tuple. */
@@ -2900,25 +3002,58 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ /* No file is exported until the worker exports the next one. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ BufFileClose(file);
}
/*
@@ -3044,15 +3179,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3067,7 +3197,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3170,7 +3299,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3266,8 +3395,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3380,3 +3512,488 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->sfs_valid = false;
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ Assert(!shared->sfs_valid);
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * The worker already had to access some system catalogs during startup,
+ * and we even had to open the relation we are processing. Now that we're
+ * going to work with historic snapshots, the system caches must be
+ * invalidated.
+ */
+ InvalidateSystemCaches();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
+
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessParallelMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res = SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 2b75de0ddef..28a5a400fb1 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 142a02eb5e9..b368990e90b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -135,6 +136,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 866f92cf799..85323ba61e2 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -205,7 +205,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 087821311cc..af12144795b 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -694,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..4a4858882f0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f39830dbb34..cbcc8550960 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -153,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index d8f76d325f9..2b983abce3e 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1652,7 +1650,7 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
* - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
+ * changes. NULL if background worker takes care of the decoding.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1664,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1672,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index b43a1740053..0ac70ec30d7 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,6 +17,7 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
#include "storage/relfilelocator.h"
#include "utils/relcache.h"
@@ -47,6 +48,9 @@ typedef struct ClusterParams
extern RelFileLocator repacked_rel_locator;
extern RelFileLocator repacked_rel_toast_locator;
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -55,68 +59,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -125,9 +91,6 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
LOCKMODE lockmode);
extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
-
extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
char relpersistence, LOCKMODE lockmode);
extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
@@ -140,4 +103,5 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
MultiXactId cutoffMulti,
char newrelpersistence);
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index afeeb1ca019..c0a66516b66 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3139b14e85f..35344910f65 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -488,7 +488,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -629,6 +628,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2025-12-13 18:27 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2025-12-13 18:27 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1144 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 68 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 946 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 6063e037edb..cb09e6fd1dc 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 569705abc81..b0383c1375f 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -61,6 +62,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +74,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -138,6 +143,106 @@ typedef struct ChangeDest
IndexInsertState *iistate;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Can backend read the file contents? */
+ bool sfs_valid;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +250,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +263,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +278,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -187,7 +290,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -197,6 +299,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +728,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1038,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1051,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
+ *
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
+ */
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1105,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -994,14 +1119,10 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
{
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1172,8 +1293,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1334,7 +1454,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2367,59 +2486,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* This function is much like pg_create_logical_replication_slot() except that
* the new slot is neither released (if anyone else could read changes from
@@ -2431,9 +2497,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2441,21 +2508,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2477,104 +2544,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2585,80 +2657,117 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible. The point is that the worker is only useful if
+ * it starts decoding before lsn_upto is set. Thus it can reach
+ * the end of WAL and find out later that it did not have to go
+ * that far.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ /* Wait a bit before we retry reading WAL. */
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 100000L, /* XXX Tune the delay. */
+ WAIT_EVENT_REPACK_WORKER_MAIN);
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2666,12 +2775,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2688,7 +2796,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2703,26 +2811,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2901,25 +3002,58 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ /* No file is exported until the worker exports the next one. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ BufFileClose(file);
}
/*
@@ -3045,15 +3179,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3068,7 +3197,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3172,7 +3300,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3268,8 +3396,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3382,3 +3513,514 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->sfs_valid = false;
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ Assert(!shared->sfs_valid);
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
+
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessParallelMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res = SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 2b75de0ddef..28a5a400fb1 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 8e1068969ae..2d1ba94a45f 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -135,6 +136,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 1b11ed63dc6..b3fd7fec392 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -205,7 +205,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 087821311cc..af12144795b 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -694,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..4a4858882f0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f39830dbb34..cbcc8550960 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -153,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index d8f76d325f9..321c00682ec 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index b43a1740053..0ac70ec30d7 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,6 +17,7 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
#include "storage/relfilelocator.h"
#include "utils/relcache.h"
@@ -47,6 +48,9 @@ typedef struct ClusterParams
extern RelFileLocator repacked_rel_locator;
extern RelFileLocator repacked_rel_toast_locator;
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -55,68 +59,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -125,9 +91,6 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
LOCKMODE lockmode);
extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
-
extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
char relpersistence, LOCKMODE lockmode);
extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
@@ -140,4 +103,5 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
MultiXactId cutoffMulti,
char newrelpersistence);
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index afeeb1ca019..c0a66516b66 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3139b14e85f..35344910f65 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -488,7 +488,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -629,6 +628,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v28-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2025-12-13 18:27 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2025-12-13 18:27 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1144 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 68 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 946 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 6063e037edb..cb09e6fd1dc 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 569705abc81..b0383c1375f 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -61,6 +62,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +74,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -138,6 +143,106 @@ typedef struct ChangeDest
IndexInsertState *iistate;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Can backend read the file contents? */
+ bool sfs_valid;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +250,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +263,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +278,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -187,7 +290,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -197,6 +299,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +728,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1038,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1051,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
+ *
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
+ */
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1105,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -994,14 +1119,10 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
{
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1172,8 +1293,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1334,7 +1454,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2367,59 +2486,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* This function is much like pg_create_logical_replication_slot() except that
* the new slot is neither released (if anyone else could read changes from
@@ -2431,9 +2497,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2441,21 +2508,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2477,104 +2544,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2585,80 +2657,117 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible. The point is that the worker is only useful if
+ * it starts decoding before lsn_upto is set. Thus it can reach
+ * the end of WAL and find out later that it did not have to go
+ * that far.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ /* Wait a bit before we retry reading WAL. */
+ (void) WaitLatch(MyLatch,
+ WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+ 100000L, /* XXX Tune the delay. */
+ WAIT_EVENT_REPACK_WORKER_MAIN);
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2666,12 +2775,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2688,7 +2796,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2703,26 +2811,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2901,25 +3002,58 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ /* No file is exported until the worker exports the next one. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ BufFileClose(file);
}
/*
@@ -3045,15 +3179,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3068,7 +3197,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3172,7 +3300,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3268,8 +3396,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3382,3 +3513,514 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->sfs_valid = false;
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ Assert(!shared->sfs_valid);
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = true;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool valid;
+
+ SpinLockAcquire(&shared->mutex);
+ valid = shared->sfs_valid;
+ SpinLockRelease(&shared->mutex);
+
+ if (valid)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ SpinLockAcquire(&shared->mutex);
+ shared->sfs_valid = false;
+ SpinLockRelease(&shared->mutex);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessParallelMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res = SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 2b75de0ddef..28a5a400fb1 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 8e1068969ae..2d1ba94a45f 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -135,6 +136,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 1b11ed63dc6..b3fd7fec392 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -205,7 +205,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 087821311cc..af12144795b 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -694,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..4a4858882f0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index f39830dbb34..cbcc8550960 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -153,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index d8f76d325f9..321c00682ec 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index b43a1740053..0ac70ec30d7 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,6 +17,7 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
#include "storage/relfilelocator.h"
#include "utils/relcache.h"
@@ -47,6 +48,9 @@ typedef struct ClusterParams
extern RelFileLocator repacked_rel_locator;
extern RelFileLocator repacked_rel_toast_locator;
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -55,68 +59,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -125,9 +91,6 @@ extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
LOCKMODE lockmode);
extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
-
extern Oid make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
char relpersistence, LOCKMODE lockmode);
extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
@@ -140,4 +103,5 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
MultiXactId cutoffMulti,
char newrelpersistence);
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index afeeb1ca019..c0a66516b66 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3139b14e85f..35344910f65 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -488,7 +488,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -629,6 +628,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v28-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-08 16:47 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-08 16:47 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1174 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 979 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 3526b6adcb5..475c536ce43 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index c3feb0c3de4..5232fbfb57d 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -187,7 +302,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -197,6 +311,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -994,14 +1131,10 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
{
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1172,8 +1305,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1334,7 +1466,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2367,59 +2498,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2484,9 +2562,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2494,21 +2573,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2530,104 +2609,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2638,80 +2722,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
- CHECK_FOR_INTERRUPTS();
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2719,12 +2860,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2741,7 +2881,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2756,26 +2896,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2954,25 +3087,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3098,15 +3265,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3121,7 +3283,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3365,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3387,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3319,8 +3481,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3430,3 +3595,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 015c67bbeba..566e5a50c30 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3299de23bb3..73a3def69bc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot synchronization."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -154,6 +155,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a0b7b38a5e2..d1a694f9008 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -496,7 +496,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -636,6 +635,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v29-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-08 16:47 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-08 16:47 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1174 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 979 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 3526b6adcb5..475c536ce43 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index c3feb0c3de4..5232fbfb57d 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -187,7 +302,6 @@ static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -197,6 +311,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -994,14 +1131,10 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
{
Assert(!swap_toast_by_content);
rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
- decoding_ctx,
frozenXid, cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1172,8 +1305,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1334,7 +1466,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2367,59 +2498,6 @@ RepackCommandAsString(RepackCommand cmd)
return "???";
}
-
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2484,9 +2562,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2494,21 +2573,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2530,104 +2609,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2638,80 +2722,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
- CHECK_FOR_INTERRUPTS();
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2719,12 +2860,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2741,7 +2881,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2756,26 +2896,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2954,25 +3087,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
- apply_concurrent_changes(dstate, dest);
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3098,15 +3265,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3121,7 +3283,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
Relation cl_index,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3365,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3387,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3319,8 +3481,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3430,3 +3595,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index c8930640a0d..fb9956d392d 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 015c67bbeba..566e5a50c30 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3299de23bb3..73a3def69bc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot synchronization."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -154,6 +155,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a0b7b38a5e2..d1a694f9008 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -496,7 +496,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -636,6 +635,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v29-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-12 16:30 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-12 16:30 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1176 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 981 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 3526b6adcb5..475c536ce43 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 9f61620a279..49a42f54ae4 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2375,58 +2507,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,21 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2537,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
- CHECK_FOR_INTERRUPTS();
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3092,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3270,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3287,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3367,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3389,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3470,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3584,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3299de23bb3..73a3def69bc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot synchronization."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -154,6 +155,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a0b7b38a5e2..d1a694f9008 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -496,7 +496,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -636,6 +635,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v30-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-12 16:30 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-12 16:30 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentiallly long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1176 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 2 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 4 +-
13 files changed, 981 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 3526b6adcb5..475c536ce43 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -957,31 +954,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1027,22 +999,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 9f61620a279..49a42f54ae4 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located int.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if its still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2375,58 +2507,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,21 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Check if we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
/*
* Neither prepare_write nor do_write callback nor update_progress is
@@ -2537,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
- CHECK_FOR_INTERRUPTS();
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3092,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3270,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3287,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3367,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3389,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3470,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3584,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 3299de23bb3..73a3def69bc 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -62,6 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process."
LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process."
LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process."
RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery."
+REPACK_WORKER_MAIN "Waiting in main loop of REPACK decoding worker process."
REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot synchronization."
REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down."
SYSLOGGER_MAIN "Waiting in main loop of syslogger process."
@@ -154,6 +155,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a0b7b38a5e2..d1a694f9008 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -496,7 +496,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -636,6 +635,9 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
+DecodingWorkerState
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v30-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-22 08:23 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-22 08:23 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 402 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5537a2d2530..56d01ccd5af 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 556128e9c57..3cae074c7e7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v31-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-22 08:23 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-22 08:23 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 402 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5537a2d2530..56d01ccd5af 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 556128e9c57..3cae074c7e7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v31-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-27 10:48 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-27 10:48 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 402 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5537a2d2530..56d01ccd5af 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1aa4fb442f1..1e2e4ba80e9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v32-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v33 5/5] Use background worker to do logical decoding.
@ 2026-01-27 10:48 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-27 10:48 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1183 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 984 insertions(+), 404 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
+
+ CHECK_FOR_INTERRUPTS();
+
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
-
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
-
- CHECK_FOR_INTERRUPTS();
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
+ }
}
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
- }
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
-
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- apply_concurrent_changes(dstate, dest);
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 261ccd3f59c..09c58371e8e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 6b54ea040ac..3a109dbfaff 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -167,17 +167,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -194,46 +190,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bca..e08bf56f11e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3520,6 +3521,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 14928cd04a1..f1005afde9d 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2676823d2a7..84c7b680dc5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--r7amdyq3rmktb267--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/6] Use background worker to do logical decoding.
@ 2026-01-27 10:48 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-27 10:48 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 4 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 402 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 65deabe91a7..334bb708c5b 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -136,6 +137,9 @@ static const struct
},
{
"SequenceSyncWorkerMain", SequenceSyncWorkerMain
+ },
+ {
+ "RepackWorkerMain", RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index b0ef1a12520..35a46988285 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index a074eed393d..4bc47a72371 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -169,17 +169,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -196,46 +192,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index e54bf1e760f..fc81ad87615 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3541,6 +3542,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 5537a2d2530..56d01ccd5af 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 76aa993009a..15760363a1a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1651,8 +1649,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1666,7 +1662,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1675,7 +1670,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1aa4fb442f1..1e2e4ba80e9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=
Content-Type: text/plain
Content-Disposition: attachment;
filename=v32-0006-Use-multiple-snapshots-to-copy-the-data.patch
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v33 5/5] Use background worker to do logical decoding.
@ 2026-01-27 10:48 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-01-27 10:48 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1183 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 984 insertions(+), 404 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 908f1ef66c6..8589b3c940e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -971,31 +968,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1041,22 +1013,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 03ccf10b782..e988a7a7296 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,113 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /* Error queue. */
+ shm_mq *error_mq;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +262,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +275,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +290,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +301,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +310,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +740,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1050,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1063,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1117,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1138,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1179,8 +1313,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1341,7 +1474,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2371,62 +2503,10 @@ RepackCommandAsString(RepackCommand cmd)
case REPACK_COMMAND_CLUSTER:
return "CLUSTER";
}
- return "???"; /* keep compiler quiet */
+ return "???"; /* keep compiler quiet */
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2491,9 +2571,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2501,20 +2582,21 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2537,104 +2619,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
+
+ CHECK_FOR_INTERRUPTS();
+
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
-
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2645,80 +2732,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
-
- CHECK_FOR_INTERRUPTS();
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
+ }
}
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
- }
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
-
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2726,12 +2870,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2748,7 +2891,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2763,26 +2906,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2957,25 +3093,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- apply_concurrent_changes(dstate, dest);
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3101,15 +3271,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3123,7 +3288,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3204,6 +3368,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3225,7 +3390,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3306,8 +3471,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3417,3 +3585,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 261ccd3f59c..09c58371e8e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 6b54ea040ac..3a109dbfaff 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -167,17 +167,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -194,46 +190,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..6f9e7a7aab7 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE))
HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE);
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bca..e08bf56f11e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3520,6 +3521,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 14928cd04a1..f1005afde9d 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..3ef35ca6b80 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
/* Recovery conflict reasons */
PROCSIG_RECOVERY_CONFLICT_FIRST,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2676823d2a7..84c7b680dc5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -498,7 +498,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -638,6 +637,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--r7amdyq3rmktb267--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v35 5/5] Use background worker to do logical decoding.
@ 2026-02-16 19:49 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-02-16 19:49 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 403 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 41c52046e14..4c503a63832 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -972,31 +969,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1042,22 +1014,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 6111f7985ab..86c0d44f2d1 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,110 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +259,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +272,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +287,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +298,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +307,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +737,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1047,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1060,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1114,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1135,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1180,8 +1311,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1342,7 +1472,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2376,58 +2505,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2492,9 +2569,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2502,20 +2580,22 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2538,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
+
+ CHECK_FOR_INTERRUPTS();
+
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
-
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2646,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
-
- CHECK_FOR_INTERRUPTS();
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
+ }
}
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
- }
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
-
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2727,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2749,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2764,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2954,25 +3088,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- apply_concurrent_changes(dstate, dest);
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3098,15 +3266,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3120,7 +3283,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3201,6 +3363,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3222,7 +3385,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3303,8 +3466,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3416,3 +3582,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 8678ea4e139..1edb77ba431 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 601016716b3..d0c464a98d5 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7505c9d3a37..7c2d637bd33 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -698,6 +699,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT))
HandleRecoveryConflictInterrupt();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d01a09dd0c4..9711abaf2eb 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3575,6 +3576,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 82aa258f72d..ce72e7e017d 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..a944ee0d211 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check
* PGPROC->pendingRecoveryConflicts for the
* reason */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 71a0802187e..b35d4e318fc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -499,7 +499,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -639,6 +638,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--kmtyk4ln3767qrma--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/5] Use background worker to do logical decoding.
@ 2026-02-16 19:49 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-02-16 19:49 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1175 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 979 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 48b66823523..e55a4bf727e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -972,31 +969,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1042,22 +1014,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index b60cda202d2..7c9945846e2 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,110 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +259,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +272,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +287,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +298,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +307,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +737,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1047,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1060,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1114,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1135,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1180,8 +1311,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1342,7 +1472,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2376,58 +2505,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2492,9 +2569,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2502,20 +2580,22 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2538,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2646,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2727,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2749,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2764,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2958,25 +3092,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3102,15 +3270,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3124,7 +3287,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3205,6 +3367,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3226,7 +3389,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3307,8 +3470,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3420,3 +3586,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 261ccd3f59c..09c58371e8e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 601016716b3..d0c464a98d5 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 5d33559926a..080da1ee490 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT))
HandleRecoveryConflictInterrupt();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21de158adbb..2f667200d0c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3575,6 +3576,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1776d0dafdf..a9874cb1309 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..a944ee0d211 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check
* PGPROC->pendingRecoveryConflicts for the
* reason */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 71a0802187e..b35d4e318fc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -499,7 +499,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -639,6 +638,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH v35 5/5] Use background worker to do logical decoding.
@ 2026-02-16 19:49 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-02-16 19:49 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1179 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 981 insertions(+), 403 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 41c52046e14..4c503a63832 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -972,31 +969,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1042,22 +1014,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 6111f7985ab..86c0d44f2d1 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,110 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +259,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +272,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +287,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +298,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +307,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +737,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1047,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1060,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1114,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1135,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1180,8 +1311,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1342,7 +1472,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2376,58 +2505,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2492,9 +2569,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2502,20 +2580,22 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2538,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
+ * If true is returned, there is no more work for the worker.
*/
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
- */
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
+
+ CHECK_FOR_INTERRUPTS();
+
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
{
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
-
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
-
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2646,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
-
- CHECK_FOR_INTERRUPTS();
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
+
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
+ }
}
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
- }
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
-
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2727,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2749,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2764,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2954,25 +3088,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- if (dstate->nchanges == 0)
- return;
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- apply_concurrent_changes(dstate, dest);
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3098,15 +3266,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3120,7 +3283,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3201,6 +3363,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3222,7 +3385,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3303,8 +3466,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3416,3 +3582,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 8678ea4e139..1edb77ba431 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 601016716b3..d0c464a98d5 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7505c9d3a37..7c2d637bd33 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -698,6 +699,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT))
HandleRecoveryConflictInterrupt();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d01a09dd0c4..9711abaf2eb 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3575,6 +3576,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 82aa258f72d..ce72e7e017d 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..a944ee0d211 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check
* PGPROC->pendingRecoveryConflicts for the
* reason */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 71a0802187e..b35d4e318fc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -499,7 +499,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -639,6 +638,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--kmtyk4ln3767qrma--
^ permalink raw reply [nested|flat] 28+ messages in thread
* [PATCH 5/5] Use background worker to do logical decoding.
@ 2026-02-16 19:49 Antonin Houska <[email protected]>
0 siblings, 0 replies; 28+ messages in thread
From: Antonin Houska @ 2026-02-16 19:49 UTC (permalink / raw)
If the backend performing REPACK (CONCURRENTLY) does both data copying and
logical decoding, it has to "travel in time" back and forth and therefore it
has to invalidate system caches quite a few times. (The copying and the
decoding work with different catalog snapshots.) As the decoding worker has
separate caches, the switching is not necessary.
Without the worker, it'd also be difficult to switch between potentially long
running tasks like index build and WAL decoding. (No decoding during that time
at all can suspend archiving / recycling of WAL segments for some time, which
in turn may result in full disk.)
Another problem is that, after having acquired AccessExclusiveLock (in order
to swap the files), the backend needs to both decode and apply the data
changes that took place while it was waiting for the lock. With the decoding
worker, the decoding runs all the time, so the backend only needs to apply the
changes. This can reduce the time the exclusive lock is held for.
Note that the code added in order to handle ERRORs in the background worker
almost duplicates the existing code that does the same for other types of
workers (See ProcessParallelMessages() and
ProcessParallelApplyMessages()). Refactoring of the existing code might be
useful, to reduce the duplication.
---
src/backend/access/heap/heapam_handler.c | 44 -
src/backend/commands/cluster.c | 1175 +++++++++++++----
src/backend/libpq/pqmq.c | 5 +
src/backend/postmaster/bgworker.c | 5 +
src/backend/replication/logical/logical.c | 6 +-
.../pgoutput_repack/pgoutput_repack.c | 54 +-
src/backend/storage/ipc/procsignal.c | 4 +
src/backend/tcop/postgres.c | 4 +
.../utils/activity/wait_event_names.txt | 1 +
src/include/access/tableam.h | 7 +-
src/include/commands/cluster.h | 71 +-
src/include/storage/procsignal.h | 1 +
src/tools/pgindent/typedefs.list | 3 +-
13 files changed, 979 insertions(+), 401 deletions(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 48b66823523..e55a4bf727e 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,7 +33,6 @@
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
-#include "commands/cluster.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
@@ -688,7 +687,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
Relation OldIndex, bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -710,7 +708,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
BufferHeapTupleTableSlot *hslot;
BlockNumber prev_cblock = InvalidBlockNumber;
bool concurrent = snapshot != NULL;
- XLogRecPtr end_of_wal_prev = GetFlushRecPtr(NULL);
/* Remember if it's a system catalog */
is_system_catalog = IsSystemRelation(OldHeap);
@@ -972,31 +969,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
ct_val[1] = *num_tuples;
pgstat_progress_update_multi_param(2, ct_index, ct_val);
}
-
- /*
- * Process the WAL produced by the load, as well as by other
- * transactions, so that the replication slot can advance and WAL does
- * not pile up. Use wal_segment_size as a threshold so that we do not
- * introduce the decoding overhead too often.
- *
- * Of course, we must not apply the changes until the initial load has
- * completed.
- *
- * Note that our insertions into the new table should not be decoded
- * as we (intentionally) do not write the logical decoding specific
- * information to WAL.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
if (indexScan != NULL)
@@ -1042,22 +1014,6 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
/* Report n_tuples */
pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
n_tuples);
-
- /*
- * Try to keep the amount of not-yet-decoded WAL small, like
- * above.
- */
- if (concurrent)
- {
- XLogRecPtr end_of_wal;
-
- end_of_wal = GetFlushRecPtr(NULL);
- if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
- {
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
- end_of_wal_prev = end_of_wal;
- }
- }
}
tuplesort_end(tuplesort);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index b60cda202d2..7c9945846e2 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -12,12 +12,13 @@
* In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
* then do an initial copy as above. However, while the tuples are being
* copied, concurrent transactions could modify the table. To cope with those
- * changes, we rely on logical decoding to obtain them from WAL. The changes
- * are accumulated in a tuplestore. Once the initial copy is complete, we
- * read the changes from the tuplestore and re-apply them on the new heap.
- * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
- * swap the relfilenodes. This way, the time we hold a strong lock on the
- * table is much reduced, and the bloat is eliminated.
+ * changes, we rely on logical decoding to obtain them from WAL. A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file. Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
*
* There is hardly anything left of Paul Brown's original implementation...
*
@@ -45,6 +46,7 @@
#include "access/xlog_internal.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
+#include "access/xlogwait.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/heap.h"
@@ -61,6 +63,8 @@
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
#include "miscadmin.h"
#include "optimizer/optimizer.h"
#include "pgstat.h"
@@ -71,6 +75,8 @@
#include "storage/ipc.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
+#include "storage/procsignal.h"
+#include "tcop/tcopprot.h"
#include "utils/acl.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
@@ -117,6 +123,12 @@ typedef struct IndexInsertState
/* The WAL segment being decoded. */
static XLogSegNo repack_current_segment = 0;
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT 0
+
/*
* Information needed to apply concurrent data changes.
*/
@@ -136,8 +148,110 @@ typedef struct ChangeDest
/* Needed to update indexes of rel_dst. */
IndexInsertState *iistate;
+
+ /*
+ * Sequential number of the file containing the changes.
+ *
+ * TODO This field makes the structure name less descriptive. Should we
+ * rename it, e.g. to ChangeApplyInfo?
+ */
+ int file_seq;
} ChangeDest;
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+ /* Is the decoding initialized? */
+ bool initialized;
+
+ /*
+ * Once the worker has reached this LSN, it should close the current
+ * output file and either create a new one or exit, according to the field
+ * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+ * the WAL available and keep checking this field. It is ok if the worker
+ * had already decoded records whose LSN is >= lsn_upto before this field
+ * has been set.
+ */
+ XLogRecPtr lsn_upto;
+
+ /* Exit after closing the current file? */
+ bool done;
+
+ /* The output is stored here. */
+ SharedFileSet sfs;
+
+ /* Number of the last file exported by the worker. */
+ int last_exported;
+
+ /* Synchronize access to the fields above. */
+ slock_t mutex;
+
+ /* Database to connect to. */
+ Oid dbid;
+
+ /* Role to connect as. */
+ Oid roleid;
+
+ /* Decode data changes of this relation. */
+ Oid relid;
+
+ /* The backend uses this to wait for the worker. */
+ ConditionVariable cv;
+
+ /* Info to signal the backend. */
+ PGPROC *backend_proc;
+ pid_t backend_pid;
+ ProcNumber backend_proc_number;
+
+ /*
+ * Memory the queue is located in.
+ *
+ * For considerations on the value see the comments of
+ * PARALLEL_ERROR_QUEUE_SIZE.
+ */
+#define REPACK_ERROR_QUEUE_SIZE 16384
+ char error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+/*
+ * Generate worker's output file name. If relations of the same 'relid' happen
+ * to be processed at the same time, they must be from different databases and
+ * therefore different backends must be involved. (PID is already present in
+ * the fileset name.)
+ */
+static inline void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+ snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+ /* The worker. */
+ BackgroundWorkerHandle *handle;
+
+ /* DecodingWorkerShared is in this segment. */
+ dsm_segment *seg;
+
+ /* Handle of the error queue. */
+ shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
Oid indexOid, Oid userid, LOCKMODE lmode,
int options);
@@ -145,7 +259,7 @@ static void check_repack_concurrently_requirements(Relation rel);
static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
bool concurrent);
static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+ Snapshot snapshot,
bool verbose,
bool *pSwapToastByContent,
TransactionId *pFreezeXid,
@@ -158,12 +272,10 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
static bool cluster_is_permitted_for_relation(RepackCommand cmd,
Oid relid, Oid userid);
-static void begin_concurrent_repack(Relation rel);
-static void end_concurrent_repack(void);
static LogicalDecodingContext *setup_logical_decoding(Oid relid);
-static HeapTuple get_changed_tuple(char *change);
-static void apply_concurrent_changes(RepackDecodingState *dstate,
- ChangeDest *dest);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared);
+static void apply_concurrent_changes(BufFile *file, ChangeDest *dest);
static void apply_concurrent_insert(Relation rel, HeapTuple tup,
IndexInsertState *iistate,
TupleTableSlot *index_slot);
@@ -175,9 +287,9 @@ static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
HeapTuple tup_key,
TupleTableSlot *ident_slot);
-static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal,
- ChangeDest *dest);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+ ChangeDest *dest,
+ bool done);
static IndexInsertState *get_index_insert_state(Relation relation,
Oid ident_index_id,
Relation *ident_index_p);
@@ -186,7 +298,6 @@ static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
static void free_index_insert_state(IndexInsertState *iistate);
static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti);
static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
@@ -196,6 +307,13 @@ static Relation process_single_relation(RepackStmt *stmt,
ClusterParams *params);
static Oid determine_clustered_index(Relation rel, bool usingindex,
const char *indexname);
+static void start_decoding_worker(Oid relid);
+static void stop_decoding_worker(void);
+static void repack_worker_internal(dsm_segment *seg);
+static void export_initial_snapshot(Snapshot snapshot,
+ DecodingWorkerShared *shared);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+static void ProcessRepackMessage(StringInfo msg);
static const char *RepackCommandAsString(RepackCommand cmd);
@@ -619,20 +737,20 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
/* rebuild_relation does all the dirty work */
PG_TRY();
{
- /*
- * For concurrent processing, make sure that our logical decoding
- * ignores data changes of other tables than the one we are
- * processing.
- */
- if (concurrent)
- begin_concurrent_repack(OldHeap);
-
rebuild_relation(OldHeap, index, verbose, concurrent);
}
PG_FINALLY();
{
if (concurrent)
- end_concurrent_repack();
+ {
+ /*
+ * Since during normal operation the worker was already asked to
+ * exit, stopping it explicitly is especially important on ERROR.
+ * However it still seems a good practice to make sure that the
+ * worker never survives the REPACK command.
+ */
+ stop_decoding_worker();
+ }
}
PG_END_TRY();
@@ -929,7 +1047,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
bool swap_toast_by_content;
TransactionId frozenXid;
MultiXactId cutoffMulti;
- LogicalDecodingContext *decoding_ctx = NULL;
Snapshot snapshot = NULL;
#if USE_ASSERT_CHECKING
LOCKMODE lmode;
@@ -943,19 +1060,36 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (concurrent)
{
/*
- * Prepare to capture the concurrent data changes.
+ * The worker needs to be member of the locking group we're the leader
+ * of. We ought to become the leader before the worker starts. The
+ * worker will join the group as soon as it starts.
*
- * Note that this call waits for all transactions with XID already
- * assigned to finish. If some of those transactions is waiting for a
- * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
- * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
- * risk is worth unlocking/locking the table (and its clustering
- * index) and checking again if its still eligible for REPACK
- * CONCURRENTLY.
+ * This is to make sure that the deadlock described below is
+ * detectable by deadlock.c: if the worker waits for a transaction to
+ * complete and we are waiting for the worker output, then effectively
+ * we (i.e. this backend) are waiting for that transaction.
*/
- decoding_ctx = setup_logical_decoding(tableOid);
+ BecomeLockGroupLeader();
+
+ /*
+ * Start the worker that decodes data changes applied while we're
+ * copying the table contents.
+ *
+ * Note that the worker has to wait for all transactions with XID
+ * already assigned to finish. If some of those transactions is
+ * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+ * table (e.g. it runs CREATE INDEX), we can end up in a deadlock.
+ * Not sure this risk is worth unlocking/locking the table (and its
+ * clustering index) and checking again if it's still eligible for
+ * REPACK CONCURRENTLY.
+ */
+ start_decoding_worker(tableOid);
+
+ /*
+ * Wait until the worker has the initial snapshot and retrieve it.
+ */
+ snapshot = get_initial_snapshot(decoding_worker);
- snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
PushActiveSnapshot(snapshot);
}
@@ -980,7 +1114,7 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
NewHeap = table_open(OIDNewHeap, NoLock);
/* Copy the heap data into the new table in the desired order */
- copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
+ copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
&swap_toast_by_content, &frozenXid, &cutoffMulti);
/* The historic snapshot won't be needed anymore. */
@@ -1001,14 +1135,11 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent
if (index)
index_close(index, NoLock);
- rebuild_relation_finish_concurrent(NewHeap, OldHeap, decoding_ctx,
- frozenXid, cutoffMulti);
+ rebuild_relation_finish_concurrent(NewHeap, OldHeap, frozenXid,
+ cutoffMulti);
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
-
- /* Done with decoding. */
- cleanup_logical_decoding(decoding_ctx);
}
else
{
@@ -1180,8 +1311,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
static void
copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
- Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
- bool verbose, bool *pSwapToastByContent,
+ Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
{
Relation relRelation;
@@ -1342,7 +1472,6 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
*/
table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
cutoffs.OldestXmin, snapshot,
- decoding_ctx,
&cutoffs.FreezeLimit,
&cutoffs.MultiXactCutoff,
&num_tuples, &tups_vacuumed,
@@ -2376,58 +2505,6 @@ RepackCommandAsString(RepackCommand cmd)
}
-/*
- * Call this function before REPACK CONCURRENTLY starts to setup logical
- * decoding. It makes sure that other users of the table put enough
- * information into WAL.
- *
- * The point is that at various places we expect that the table we're
- * processing is treated like a system catalog. For example, we need to be
- * able to scan it using a "historic snapshot" anytime during the processing
- * (as opposed to scanning only at the start point of the decoding, as logical
- * replication does during initial table synchronization), in order to apply
- * concurrent UPDATE / DELETE commands.
- *
- * Note that TOAST table needs no attention here as it's not scanned using
- * historic snapshot.
- */
-static void
-begin_concurrent_repack(Relation rel)
-{
- Oid toastrelid;
-
- /*
- * Avoid logical decoding of other relations by this backend. The lock we
- * have guarantees that the actual locator cannot be changed concurrently:
- * TRUNCATE needs AccessExclusiveLock.
- */
- Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
- repacked_rel_locator = rel->rd_locator;
- toastrelid = rel->rd_rel->reltoastrelid;
- if (OidIsValid(toastrelid))
- {
- Relation toastrel;
-
- /* Avoid logical decoding of other TOAST relations. */
- toastrel = table_open(toastrelid, AccessShareLock);
- repacked_rel_toast_locator = toastrel->rd_locator;
- table_close(toastrel, AccessShareLock);
- }
-}
-
-/*
- * Call this when done with REPACK CONCURRENTLY.
- */
-static void
-end_concurrent_repack(void)
-{
- /*
- * Restore normal function of (future) logical decoding for this backend.
- */
- repacked_rel_locator.relNumber = InvalidOid;
- repacked_rel_toast_locator.relNumber = InvalidOid;
-}
-
/*
* Is this backend performing logical decoding on behalf of REPACK
* (CONCURRENTLY) ?
@@ -2492,9 +2569,10 @@ static LogicalDecodingContext *
setup_logical_decoding(Oid relid)
{
Relation rel;
- TupleDesc tupdesc;
+ Oid toastrelid;
LogicalDecodingContext *ctx;
- RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+ NameData slotname;
+ RepackDecodingState *dstate;
/*
* REPACK CONCURRENTLY is not allowed in a transaction block, so this
@@ -2502,20 +2580,22 @@ setup_logical_decoding(Oid relid)
*/
Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
- /*
- * A single backend should not execute multiple REPACK commands at a time,
- * so use PID to make the slot unique.
- */
- snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
-
/*
* Make sure we can use logical decoding.
*/
CheckSlotPermissions();
CheckLogicalDecodingRequirements();
- /* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
- ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
- false, false, false);
+
+ /*
+ * A single backend should not execute multiple REPACK commands at a time,
+ * so use PID to make the slot unique.
+ *
+ * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+ */
+ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+ ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+ false);
+
EnsureLogicalDecodingEnabled();
/*
@@ -2538,104 +2618,109 @@ setup_logical_decoding(Oid relid)
DecodingContextFindStartpoint(ctx);
+ /*
+ * decode_concurrent_changes() needs non-blocking callback.
+ */
+ ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+ /*
+ * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+ * WAL.
+ */
+ ctx->reader->private_data = MemoryContextAllocZero(ctx->context,
+ sizeof(ReadLocalXLogPageNoWaitPrivate));
+
+
/* Some WAL records should have been read. */
Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+ /*
+ * Initialize repack_current_segment so that we can notice WAL segment
+ * boundaries.
+ */
XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
wal_segment_size);
- /*
- * Setup structures to store decoded changes.
- */
+ dstate = palloc0_object(RepackDecodingState);
dstate->relid = relid;
- dstate->tstore = tuplestore_begin_heap(false, false,
- maintenance_work_mem);
- /* Caller should already have the table locked. */
- rel = table_open(relid, NoLock);
- tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- dstate->tupdesc = tupdesc;
- table_close(rel, NoLock);
+ /*
+ * Tuple descriptor may be needed to flatten a tuple before we write it to
+ * a file. A copy is needed because the decoding worker invalidates system
+ * caches before it starts to do the actual work.
+ */
+ rel = table_open(relid, AccessShareLock);
+ dstate->tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
- /* Initialize the descriptor to store the changes ... */
- dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+ /* Avoid logical decoding of other relations. */
+ repacked_rel_locator = rel->rd_locator;
+ toastrelid = rel->rd_rel->reltoastrelid;
+ if (OidIsValid(toastrelid))
+ {
+ Relation toastrel;
- TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
- /* ... as well as the corresponding slot. */
- dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
- &TTSOpsMinimalTuple);
+ /* Avoid logical decoding of other TOAST relations. */
+ toastrel = table_open(toastrelid, AccessShareLock);
+ repacked_rel_toast_locator = toastrel->rd_locator;
+ table_close(toastrel, AccessShareLock);
+ }
+ table_close(rel, AccessShareLock);
- dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
- "logical decoding");
+ /* The file will be set as soon as we have it opened. */
+ dstate->file = NULL;
ctx->output_writer_private = dstate;
+
return ctx;
}
/*
- * Retrieve tuple from ConcurrentChange structure.
+ * Decode logical changes from the WAL sequence and store them to a file.
*
- * The input data starts with the structure but it might not be appropriately
- * aligned.
- */
-static HeapTuple
-get_changed_tuple(char *change)
-{
- HeapTupleData tup_data;
- HeapTuple result;
- char *src;
-
- /*
- * Ensure alignment before accessing the fields. (This is why we can't use
- * heap_copytuple() instead of this function.)
- */
- src = change + offsetof(ConcurrentChange, tup_data);
- memcpy(&tup_data, src, sizeof(HeapTupleData));
-
- result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
- memcpy(result, &tup_data, sizeof(HeapTupleData));
- result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
- src = change + SizeOfConcurrentChange;
- memcpy(result->t_data, src, result->t_len);
-
- return result;
-}
-
-/*
- * Decode logical changes from the WAL sequence up to end_of_wal.
+ * If true is returned, there is no more work for the worker.
*/
-void
-repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal)
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+ DecodingWorkerShared *shared)
{
RepackDecodingState *dstate;
- ResourceOwner resowner_old;
+ XLogRecPtr lsn_upto;
+ bool done;
+ char fname[MAXPGPATH];
dstate = (RepackDecodingState *) ctx->output_writer_private;
- resowner_old = CurrentResourceOwner;
- CurrentResourceOwner = dstate->resowner;
- PG_TRY();
+ /* Open the output file. */
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+
+ while (true)
{
- while (ctx->reader->EndRecPtr < end_of_wal)
- {
- XLogRecord *record;
- XLogSegNo segno_new;
- char *errm = NULL;
- XLogRecPtr end_lsn;
+ XLogRecord *record;
+ XLogSegNo segno_new;
+ char *errm = NULL;
+ XLogRecPtr end_lsn;
- record = XLogReadRecord(ctx->reader, &errm);
- if (errm)
- elog(ERROR, "%s", errm);
+ CHECK_FOR_INTERRUPTS();
- if (record != NULL)
- LogicalDecodingProcessRecord(ctx, ctx->reader);
+ record = XLogReadRecord(ctx->reader, &errm);
+ if (record)
+ {
+ LogicalDecodingProcessRecord(ctx, ctx->reader);
/*
* If WAL segment boundary has been crossed, inform the decoding
- * system that the catalog_xmin can advance. (We can confirm more
- * often, but a filling a single WAL segment should not take much
- * time.)
+ * system that the catalog_xmin can advance.
+ *
+ * TODO Does it make sense to confirm more often? Segment size
+ * seems appropriate for restart_lsn (because less than a segment
+ * cannot be recycled anyway), however more frequent checks might
+ * be beneficial for catalog_xmin.
*/
end_lsn = ctx->reader->EndRecPtr;
XLByteToSeg(end_lsn, segno_new, wal_segment_size);
@@ -2646,80 +2731,137 @@ repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
(uint32) (end_lsn >> 32), (uint32) end_lsn);
repack_current_segment = segno_new;
}
+ }
+ else
+ {
+ ReadLocalXLogPageNoWaitPrivate *priv;
+
+ if (errm)
+ ereport(ERROR, (errmsg("%s", errm)));
- CHECK_FOR_INTERRUPTS();
+ /*
+ * In the decoding loop we do not want to get blocked when there
+ * is no more WAL available, otherwise the loop would become
+ * uninterruptible.
+ */
+ priv = (ReadLocalXLogPageNoWaitPrivate *)
+ ctx->reader->private_data;
+ if (priv->end_of_wal)
+ /* Do not miss the end of WAL condition next time. */
+ priv->end_of_wal = false;
+ else
+ ereport(ERROR, (errmsg("could not read WAL record")));
+ }
+
+ /*
+ * Whether we could read new record or not, keep checking if
+ * 'lsn_upto' was specified.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ {
+ SpinLockAcquire(&shared->mutex);
+ lsn_upto = shared->lsn_upto;
+ /* 'done' should be set at the same time as 'lsn_upto' */
+ done = shared->done;
+ SpinLockRelease(&shared->mutex);
+ }
+ if (!XLogRecPtrIsInvalid(lsn_upto) &&
+ ctx->reader->EndRecPtr >= lsn_upto)
+ break;
+
+ if (record == NULL)
+ {
+ int64 timeout = 0;
+ WaitLSNResult res;
+
+ /*
+ * Before we retry reading, wait until new WAL is flushed.
+ *
+ * There is a race condition such that the backend executing
+ * REPACK determines 'lsn_upto', but before it sets the shared
+ * variable, we reach the end of WAL. In that case we'd need to
+ * wait until the next WAL flush (unrelated to REPACK). Although
+ * that should not be a problem in a busy system, it might be
+ * noticeable in other cases, including regression tests (which
+ * are not necessarily executed in parallel). Therefore it makes
+ * sense to use timeout.
+ *
+ * If lsn_upto is valid, WAL records having LSN lower than that
+ * should already have been flushed to disk.
+ */
+ if (XLogRecPtrIsInvalid(lsn_upto))
+ timeout = 100L;
+ res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+ ctx->reader->EndRecPtr + 1,
+ timeout);
+ if (res != WAIT_LSN_RESULT_SUCCESS &&
+ res != WAIT_LSN_RESULT_TIMEOUT)
+ ereport(ERROR, (errmsg("waiting for WAL failed")));
}
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- }
- PG_CATCH();
- {
- /* clear all timetravel entries */
- InvalidateSystemCaches();
- CurrentResourceOwner = resowner_old;
- PG_RE_THROW();
}
- PG_END_TRY();
+
+ /*
+ * Close the file so we can make it available to the backend.
+ */
+ BufFileClose(dstate->file);
+ dstate->file = NULL;
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ return done;
}
/*
* Apply changes stored in 'file'.
*/
static void
-apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+apply_concurrent_changes(BufFile *file, ChangeDest *dest)
{
+ char kind;
+ uint32 t_len;
Relation rel = dest->rel;
TupleTableSlot *index_slot,
*ident_slot;
HeapTuple tup_old = NULL;
- if (dstate->nchanges == 0)
- return;
-
/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
- index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+ index_slot = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+ &TTSOpsHeapTuple);
/* A slot to fetch tuples from identity index. */
ident_slot = table_slot_create(rel, NULL);
- while (tuplestore_gettupleslot(dstate->tstore, true, false,
- dstate->tsslot))
+ while (true)
{
- bool shouldFree;
- HeapTuple tup_change,
- tup,
+ size_t nread;
+ HeapTuple tup,
tup_exist;
- char *change_raw,
- *src;
- ConcurrentChange change;
- bool isnull[1];
- Datum values[1];
CHECK_FOR_INTERRUPTS();
- /* Get the change from the single-column tuple. */
- tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
- heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
- Assert(!isnull[0]);
-
- /* Make sure we access aligned data. */
- change_raw = (char *) DatumGetByteaP(values[0]);
- src = (char *) VARDATA(change_raw);
- memcpy(&change, src, SizeOfConcurrentChange);
+ nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+ /* Are we done with the file? */
+ if (nread == 0)
+ break;
- /*
- * Extract the tuple from the change. The tuple is copied here because
- * it might be assigned to 'tup_old', in which case it needs to
- * survive into the next iteration.
- */
- tup = get_changed_tuple(src);
+ /* Read the tuple. */
+ BufFileReadExact(file, &t_len, sizeof(t_len));
+ tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+ tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+ BufFileReadExact(file, tup->t_data, t_len);
+ tup->t_len = t_len;
+ ItemPointerSetInvalid(&tup->t_self);
+ tup->t_tableOid = RelationGetRelid(dest->rel);
- if (change.kind == CHANGE_UPDATE_OLD)
+ if (kind == CHANGE_UPDATE_OLD)
{
Assert(tup_old == NULL);
tup_old = tup;
}
- else if (change.kind == CHANGE_INSERT)
+ else if (kind == CHANGE_INSERT)
{
Assert(tup_old == NULL);
@@ -2727,12 +2869,11 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
- else if (change.kind == CHANGE_UPDATE_NEW ||
- change.kind == CHANGE_DELETE)
+ else if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE)
{
HeapTuple tup_key;
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
{
tup_key = tup_old != NULL ? tup_old : tup;
}
@@ -2749,7 +2890,7 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
if (tup_exist == NULL)
elog(ERROR, "failed to find target tuple");
- if (change.kind == CHANGE_UPDATE_NEW)
+ if (kind == CHANGE_UPDATE_NEW)
apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
index_slot);
else
@@ -2764,26 +2905,19 @@ apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
pfree(tup);
}
else
- elog(ERROR, "unrecognized kind of change: %d", change.kind);
+ elog(ERROR, "unrecognized kind of change: %d", kind);
/*
* If a change was applied now, increment CID for next writes and
* update the snapshot so it sees the changes we've applied so far.
*/
- if (change.kind != CHANGE_UPDATE_OLD)
+ if (kind != CHANGE_UPDATE_OLD)
{
CommandCounterIncrement();
UpdateActiveSnapshotCommandId();
}
-
- /* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
- Assert(shouldFree);
- pfree(tup_change);
}
- tuplestore_clear(dstate->tstore);
- dstate->nchanges = 0;
-
/* Cleanup. */
ExecDropSingleTupleTableSlot(index_slot);
ExecDropSingleTupleTableSlot(ident_slot);
@@ -2958,25 +3092,59 @@ find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
}
/*
- * Decode and apply concurrent changes.
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
*/
static void
-process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
- XLogRecPtr end_of_wal, ChangeDest *dest)
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeDest *dest, bool done)
{
- RepackDecodingState *dstate;
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
PROGRESS_REPACK_PHASE_CATCH_UP);
- dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+ /* Ask the worker for the file. */
+ shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+ SpinLockAcquire(&shared->mutex);
+ shared->lsn_upto = end_of_wal;
+ shared->done = done;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * The worker needs to finish processing of the current WAL record. Even
+ * if it's idle, it'll need to close the output file. Thus we're likely to
+ * wait, so prepare for sleep.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
- repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
- if (dstate->nchanges == 0)
- return;
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == dest->file_seq)
+ break;
- apply_concurrent_changes(dstate, dest);
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Open the file. */
+ DecodingWorkerFileName(fname, shared->relid, dest->file_seq);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ apply_concurrent_changes(file, dest);
+
+ BufFileClose(file);
+
+ /* Get ready for the next file. */
+ dest->file_seq++;
}
/*
@@ -3102,15 +3270,10 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
dstate = (RepackDecodingState *) ctx->output_writer_private;
- ExecDropSingleTupleTableSlot(dstate->tsslot);
- FreeTupleDesc(dstate->tupdesc_change);
FreeTupleDesc(dstate->tupdesc);
- tuplestore_end(dstate->tstore);
-
FreeDecodingContext(ctx);
- ReplicationSlotRelease();
- ReplicationSlotDrop(NameStr(dstate->slotname), false);
+ ReplicationSlotDropAcquired();
pfree(dstate);
}
@@ -3124,7 +3287,6 @@ cleanup_logical_decoding(LogicalDecodingContext *ctx)
*/
static void
rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
- LogicalDecodingContext *decoding_ctx,
TransactionId frozenXid,
MultiXactId cutoffMulti)
{
@@ -3205,6 +3367,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
&chgdst.ident_index);
chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
&chgdst.ident_key_nentries);
+ chgdst.file_seq = WORKER_FILE_SNAPSHOT + 1;
/*
* During testing, wait for another backend to perform concurrent data
@@ -3226,7 +3389,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
* hold AccessExclusiveLock. (Quite some amount of WAL could have been
* written during the data copying and index creation.)
*/
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ process_concurrent_changes(end_of_wal, &chgdst, false);
/*
* Acquire AccessExclusiveLock on the table, its TOAST relation (if there
@@ -3307,8 +3470,11 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
XLogFlush(wal_insert_ptr);
end_of_wal = GetFlushRecPtr(NULL);
- /* Apply the concurrent changes again. */
- process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+ /*
+ * Apply the concurrent changes again. Indicate that the decoding worker
+ * won't be needed anymore.
+ */
+ process_concurrent_changes(end_of_wal, &chgdst, true);
/* Remember info about rel before closing OldHeap */
relpersistence = OldHeap->rd_rel->relpersistence;
@@ -3420,3 +3586,510 @@ build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
return result;
}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_decoding_worker(Oid relid)
+{
+ Size size;
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+ BackgroundWorker bgw;
+
+ /* Setup shared memory. */
+ size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+ BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+ seg = dsm_create(size, 0);
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+ shared->lsn_upto = InvalidXLogRecPtr;
+ shared->done = false;
+ SharedFileSetInit(&shared->sfs, seg);
+ shared->last_exported = -1;
+ SpinLockInit(&shared->mutex);
+ shared->dbid = MyDatabaseId;
+
+ /*
+ * This is the UserId set in cluster_rel(). Security context shouldn't be
+ * needed for decoding worker.
+ */
+ shared->roleid = GetUserId();
+ shared->relid = relid;
+ ConditionVariableInit(&shared->cv);
+ shared->backend_proc = MyProc;
+ shared->backend_pid = MyProcPid;
+ shared->backend_proc_number = MyProcNumber;
+
+ mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+ REPACK_ERROR_QUEUE_SIZE);
+ shm_mq_set_receiver(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+
+ memset(&bgw, 0, sizeof(bgw));
+ snprintf(bgw.bgw_name, BGW_MAXLEN,
+ "REPACK decoding worker for relation \"%s\"",
+ get_rel_name(relid));
+ snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+ bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+ BGWORKER_BACKEND_DATABASE_CONNECTION;
+ bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+ bgw.bgw_restart_time = BGW_NEVER_RESTART;
+ snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+ snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+ bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+ bgw.bgw_notify_pid = MyProcPid;
+
+ decoding_worker = palloc0_object(DecodingWorker);
+ if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+ errmsg("out of background worker slots"),
+ errhint("You might need to increase \"%s\".", "max_worker_processes")));
+
+ decoding_worker->seg = seg;
+ decoding_worker->error_mqh = mqh;
+
+ /*
+ * The decoding setup must be done before the caller can have XID assigned
+ * for any reason, otherwise the worker might end up in a deadlock,
+ * waiting for the caller's transaction to end. Therefore wait here until
+ * the worker indicates that it has the logical decoding initialized.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ bool initialized;
+
+ SpinLockAcquire(&shared->mutex);
+ initialized = shared->initialized;
+ SpinLockRelease(&shared->mutex);
+
+ if (initialized)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_decoding_worker(void)
+{
+ BgwHandleStatus status;
+
+ /* Haven't reached the worker startup? */
+ if (decoding_worker == NULL)
+ return;
+
+ /* Could not register the worker? */
+ if (decoding_worker->handle == NULL)
+ return;
+
+ TerminateBackgroundWorker(decoding_worker->handle);
+ /* The worker should really exit before the REPACK command does. */
+ HOLD_INTERRUPTS();
+ status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+ RESUME_INTERRUPTS();
+
+ if (status == BGWH_POSTMASTER_DIED)
+ ereport(FATAL,
+ (errcode(ERRCODE_ADMIN_SHUTDOWN),
+ errmsg("postmaster exited during REPACK command")));
+
+ shm_mq_detach(decoding_worker->error_mqh);
+
+ /*
+ * If we could not cancel the current sleep due to ERROR, do that before
+ * we detach from the shared memory the condition variable is located in.
+ * If we did not, the bgworker ERROR handling code would try and fail
+ * badly.
+ */
+ ConditionVariableCancelSleep();
+
+ dsm_detach(decoding_worker->seg);
+ pfree(decoding_worker);
+ decoding_worker = NULL;
+}
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+static pid_t backend_pid;
+static ProcNumber backend_proc_number;
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+ SendProcSignal(backend_pid,
+ PROCSIG_REPACK_MESSAGE,
+ backend_proc_number);
+
+ dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+ dsm_segment *seg;
+ DecodingWorkerShared *shared;
+ shm_mq *mq;
+ shm_mq_handle *mqh;
+
+ is_repack_worker = true;
+
+ /*
+ * Override the default bgworker_die() with die() so we can use
+ * CHECK_FOR_INTERRUPTS().
+ */
+ pqsignal(SIGTERM, die);
+ BackgroundWorkerUnblockSignals();
+
+ seg = dsm_attach(DatumGetUInt32(main_arg));
+ if (seg == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("could not map dynamic shared memory segment")));
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /* Arrange to signal the leader if we exit. */
+ backend_pid = shared->backend_pid;
+ backend_proc_number = shared->backend_proc_number;
+ before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+ /*
+ * Join locking group - see the comments around the call of
+ * start_decoding_worker().
+ */
+ if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+ /* The leader is not running anymore. */
+ return;
+
+ /*
+ * Setup a queue to send error messages to the backend that launched this
+ * worker.
+ */
+ mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+ shm_mq_set_sender(mq, MyProc);
+ mqh = shm_mq_attach(mq, seg, NULL);
+ pq_redirect_to_shm_mq(seg, mqh);
+ pq_set_parallel_leader(shared->backend_pid,
+ shared->backend_proc_number);
+
+ /* Connect to the database. */
+ BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+ repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+ DecodingWorkerShared *shared;
+ LogicalDecodingContext *decoding_ctx;
+ SharedFileSet *sfs;
+ Snapshot snapshot;
+
+ /*
+ * Transaction is needed to open relation, and it also provides us with a
+ * resource owner.
+ */
+ StartTransactionCommand();
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+ /*
+ * Not sure the spinlock is needed here - the backend should not change
+ * anything in the shared memory until we have serialized the snapshot.
+ */
+ SpinLockAcquire(&shared->mutex);
+ Assert(XLogRecPtrIsInvalid(shared->lsn_upto));
+ sfs = &shared->sfs;
+ SpinLockRelease(&shared->mutex);
+
+ SharedFileSetAttach(sfs, seg);
+
+ /*
+ * Prepare to capture the concurrent data changes ourselves.
+ */
+ decoding_ctx = setup_logical_decoding(shared->relid);
+
+ /* Announce that we're ready. */
+ SpinLockAcquire(&shared->mutex);
+ shared->initialized = true;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+
+ /* Build the initial snapshot and export it. */
+ snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+ export_initial_snapshot(snapshot, shared);
+
+ /*
+ * Only historic snapshots should be used now. Do not let us restrict the
+ * progress of xmin horizon.
+ */
+ InvalidateCatalogSnapshot();
+
+ while (!decode_concurrent_changes(decoding_ctx, shared))
+ ;
+
+ /* Cleanup. */
+ cleanup_logical_decoding(decoding_ctx);
+ CommitTransactionCommand();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+
+ snap_size = EstimateSnapshotSpace(snapshot);
+ snap_space = (char *) palloc(snap_size);
+ SerializeSnapshot(snapshot, snap_space);
+ FreeSnapshot(snapshot);
+
+ DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+ file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+ /* To make restoration easier, write the snapshot size first. */
+ BufFileWrite(file, &snap_size, sizeof(snap_size));
+ BufFileWrite(file, snap_space, snap_size);
+ pfree(snap_space);
+ BufFileClose(file);
+
+ /* Increase the counter to tell the backend that the file is available. */
+ SpinLockAcquire(&shared->mutex);
+ shared->last_exported++;
+ SpinLockRelease(&shared->mutex);
+ ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+ DecodingWorkerShared *shared;
+ char fname[MAXPGPATH];
+ BufFile *file;
+ Size snap_size;
+ char *snap_space;
+ Snapshot snapshot;
+
+ shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+ /*
+ * The worker needs to initialize the logical decoding, which usually
+ * takes some time. Therefore it makes sense to prepare for the sleep
+ * first.
+ */
+ ConditionVariablePrepareToSleep(&shared->cv);
+ for (;;)
+ {
+ int last_exported;
+
+ SpinLockAcquire(&shared->mutex);
+ last_exported = shared->last_exported;
+ SpinLockRelease(&shared->mutex);
+
+ /*
+ * Has the worker exported the file we are waiting for?
+ */
+ if (last_exported == WORKER_FILE_SNAPSHOT)
+ break;
+
+ ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+ }
+ ConditionVariableCancelSleep();
+
+ /* Read the snapshot from a file. */
+ DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+ file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+ BufFileReadExact(file, &snap_size, sizeof(snap_size));
+ snap_space = (char *) palloc(snap_size);
+ BufFileReadExact(file, snap_space, snap_size);
+ BufFileClose(file);
+
+ /* Restore it. */
+ snapshot = RestoreSnapshot(snap_space);
+ pfree(snap_space);
+
+ return snapshot;
+}
+
+bool
+IsRepackWorker(void)
+{
+ return is_repack_worker;
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler! All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+ InterruptPending = true;
+ RepackMessagePending = true;
+ SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from parallel workers.
+ */
+void
+ProcessRepackMessages(void)
+{
+ MemoryContext oldcontext;
+
+ static MemoryContext hpm_context = NULL;
+
+ /*
+ * Nothing to do if we haven't launched the worker yet or have already
+ * terminated it.
+ */
+ if (decoding_worker == NULL)
+ return;
+
+ /*
+ * This is invoked from ProcessInterrupts(), and since some of the
+ * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+ * for recursive calls if more signals are received while this runs. It's
+ * unclear that recursive entry would be safe, and it doesn't seem useful
+ * even if it is safe, so let's block interrupts until done.
+ */
+ HOLD_INTERRUPTS();
+
+ /*
+ * Moreover, CurrentMemoryContext might be pointing almost anywhere. We
+ * don't want to risk leaking data into long-lived contexts, so let's do
+ * our work here in a private context that we can reset on each use.
+ */
+ if (hpm_context == NULL) /* first time through? */
+ hpm_context = AllocSetContextCreate(TopMemoryContext,
+ "ProcessRepackMessages",
+ ALLOCSET_DEFAULT_SIZES);
+ else
+ MemoryContextReset(hpm_context);
+
+ oldcontext = MemoryContextSwitchTo(hpm_context);
+
+ /* OK to process messages. Reset the flag saying there are more to do. */
+ RepackMessagePending = false;
+
+ /*
+ * Read as many messages as we can from each worker, but stop when no more
+ * messages can be read from the worker without blocking.
+ */
+ while (true)
+ {
+ shm_mq_result res;
+ Size nbytes;
+ void *data;
+
+ res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+ &data, true);
+ if (res == SHM_MQ_WOULD_BLOCK)
+ break;
+ else if (res == SHM_MQ_SUCCESS)
+ {
+ StringInfoData msg;
+
+ initStringInfo(&msg);
+ appendBinaryStringInfo(&msg, data, nbytes);
+ ProcessRepackMessage(&msg);
+ pfree(msg.data);
+ }
+ else
+ {
+ /*
+ * The decoding worker is special in that it exits as soon as it
+ * has its work done. Thus the DETACHED result code is fine.
+ */
+ Assert(res == SHM_MQ_DETACHED);
+
+ break;
+ }
+ }
+
+ MemoryContextSwitchTo(oldcontext);
+
+ /* Might as well clear the context on our way out */
+ MemoryContextReset(hpm_context);
+
+ RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+ char msgtype;
+
+ msgtype = pq_getmsgbyte(msg);
+
+ switch (msgtype)
+ {
+ case PqMsg_ErrorResponse:
+ case PqMsg_NoticeResponse:
+ {
+ ErrorData edata;
+
+ /* Parse ErrorResponse or NoticeResponse. */
+ pq_parse_errornotice(msg, &edata);
+
+ /* Death of a worker isn't enough justification for suicide. */
+ edata.elevel = Min(edata.elevel, ERROR);
+
+ /*
+ * If desired, add a context line to show that this is a
+ * message propagated from a parallel worker. Otherwise, it
+ * can sometimes be confusing to understand what actually
+ * happened.
+ */
+ if (edata.context)
+ edata.context = psprintf("%s\n%s", edata.context,
+ _("decoding worker"));
+ else
+ edata.context = pstrdup(_("decoding worker"));
+
+ /* Rethrow error or print notice. */
+ ThrowErrorData(&edata);
+
+ break;
+ }
+
+ default:
+ {
+ elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+ msgtype, msg->len);
+ }
+ }
+}
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 6e4bbfb5aa1..42f6fa472c5 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqmq.h"
@@ -175,6 +176,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
SendProcSignal(pq_mq_parallel_leader_pid,
PROCSIG_PARALLEL_APPLY_MESSAGE,
pq_mq_parallel_leader_proc_number);
+ else if (IsRepackWorker())
+ SendProcSignal(pq_mq_parallel_leader_pid,
+ PROCSIG_REPACK_MESSAGE,
+ pq_mq_parallel_leader_proc_number);
else
{
Assert(IsParallelWorker());
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 261ccd3f59c..09c58371e8e 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
#include "postgres.h"
#include "access/parallel.h"
+#include "commands/cluster.h"
#include "libpq/pqsignal.h"
#include "miscadmin.h"
#include "pgstat.h"
@@ -142,6 +143,10 @@ static const struct
{
.fn_name = "SequenceSyncWorkerMain",
.fn_addr = SequenceSyncWorkerMain
+ },
+ {
+ .fn_name = "RepackWorkerMain",
+ .fn_addr = RepackWorkerMain
}
};
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
ctx->slot = slot;
- ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+ /*
+ * TODO A separate patch for PG core, unless there's really a reason to
+ * pass ctx for private_data (May extensions expect ctx?).
+ */
+ ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
if (!ctx->reader)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
index 601016716b3..d0c464a98d5 100644
--- a/src/backend/replication/pgoutput_repack/pgoutput_repack.c
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -168,17 +168,13 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
HeapTuple tuple)
{
RepackDecodingState *dstate;
- char *change_raw;
- ConcurrentChange change;
+ char kind_byte = (char) kind;
bool flattened = false;
- Size size;
- Datum values[1];
- bool isnull[1];
- char *dst;
dstate = (RepackDecodingState *) ctx->output_writer_private;
- size = VARHDRSZ + SizeOfConcurrentChange;
+ /* Store the change kind. */
+ BufFileWrite(dstate->file, &kind_byte, 1);
/*
* ReorderBufferCommit() stores the TOAST chunks in its private memory
@@ -195,46 +191,12 @@ store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
flattened = true;
}
+ /* Store the tuple size ... */
+ BufFileWrite(dstate->file, &tuple->t_len, sizeof(tuple->t_len));
+ /* ... and the tuple itself. */
+ BufFileWrite(dstate->file, tuple->t_data, tuple->t_len);
- size += tuple->t_len;
- if (size >= MaxAllocSize)
- elog(ERROR, "Change is too big.");
-
- /* Construct the change. */
- change_raw = (char *) palloc0(size);
- SET_VARSIZE(change_raw, size);
-
- /*
- * Since the varlena alignment might not be sufficient for the structure,
- * set the fields in a local instance and remember where it should
- * eventually be copied.
- */
- change.kind = kind;
- dst = (char *) VARDATA(change_raw);
-
- /*
- * Copy the tuple.
- *
- * Note: change->tup_data.t_data must be fixed on retrieval!
- */
- memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
- memcpy(dst, &change, SizeOfConcurrentChange);
- dst += SizeOfConcurrentChange;
- memcpy(dst, tuple->t_data, tuple->t_len);
-
- /* The data has been copied. */
+ /* Free the flat copy if created above. */
if (flattened)
pfree(tuple);
-
- /* Store as tuple of 1 bytea column. */
- values[0] = PointerGetDatum(change_raw);
- isnull[0] = false;
- tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
- values, isnull);
-
- /* Accounting. */
- dstate->nchanges++;
-
- /* Cleanup. */
- pfree(change_raw);
}
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 5d33559926a..080da1ee490 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
#include "access/parallel.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "port/pg_bitutils.h"
@@ -697,6 +698,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
HandleParallelApplyMessageInterrupt();
+ if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+ HandleRepackMessageInterrupt();
+
if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT))
HandleRecoveryConflictInterrupt();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21de158adbb..2f667200d0c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/async.h"
+#include "commands/cluster.h"
#include "commands/event_trigger.h"
#include "commands/explain_state.h"
#include "commands/prepare.h"
@@ -3575,6 +3576,9 @@ ProcessInterrupts(void)
if (ParallelApplyMessagePending)
ProcessParallelApplyMessages();
+
+ if (RepackMessagePending)
+ ProcessRepackMessages();
}
/*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..b00bd794759 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu
RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace."
RECOVERY_END_COMMAND "Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
RECOVERY_PAUSE "Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file."
REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped."
REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped."
RESTORE_COMMAND "Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 1776d0dafdf..a9874cb1309 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,7 +22,6 @@
#include "access/xact.h"
#include "commands/vacuum.h"
#include "executor/tuptable.h"
-#include "replication/logical.h"
#include "storage/read_stream.h"
#include "utils/rel.h"
#include "utils/snapshot.h"
@@ -631,7 +630,6 @@ typedef struct TableAmRoutine
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1663,8 +1661,6 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
* - *multi_cutoff - ditto
* - snapshot - if != NULL, ignore data changes done by transactions that this
* (MVCC) snapshot considers still in-progress or in the future.
- * - decoding_ctx - logical decoding context, to capture concurrent data
- * changes.
*
* Output parameters:
* - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1678,7 +1674,6 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
bool use_sort,
TransactionId OldestXmin,
Snapshot snapshot,
- LogicalDecodingContext *decoding_ctx,
TransactionId *xid_cutoff,
MultiXactId *multi_cutoff,
double *num_tuples,
@@ -1687,7 +1682,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
{
OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
use_sort, OldestXmin,
- snapshot, decoding_ctx,
+ snapshot,
xid_cutoff, multi_cutoff,
num_tuples, tups_vacuumed,
tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 6a5c476294a..1b05d5d418b 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -17,11 +17,13 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
#include "replication/decode.h"
+#include "postmaster/bgworker.h"
#include "replication/logical.h"
+#include "storage/buffile.h"
#include "storage/lock.h"
+#include "storage/shm_mq.h"
#include "utils/relcache.h"
#include "utils/resowner.h"
-#include "utils/tuplestore.h"
/* flag bits for ClusterParams->options */
@@ -44,6 +46,9 @@ typedef struct ClusterParams
* The following definitions are used by REPACK CONCURRENTLY.
*/
+/*
+ * Stored as a single byte in the output file.
+ */
typedef enum
{
CHANGE_INSERT,
@@ -52,68 +57,30 @@ typedef enum
CHANGE_DELETE
} ConcurrentChangeKind;
-typedef struct ConcurrentChange
-{
- /* See the enum above. */
- ConcurrentChangeKind kind;
-
- /*
- * The actual tuple.
- *
- * The tuple data follows the ConcurrentChange structure. Before use make
- * sure the tuple is correctly aligned (ConcurrentChange can be stored as
- * bytea) and that tuple->t_data is fixed.
- */
- HeapTupleData tup_data;
-} ConcurrentChange;
-
-#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
- sizeof(HeapTupleData))
-
/*
* Logical decoding state.
*
- * Here we store the data changes that we decode from WAL while the table
- * contents is being copied to a new storage. Also the necessary metadata
- * needed to apply these changes to the table is stored here.
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
*/
typedef struct RepackDecodingState
{
/* The relation whose changes we're decoding. */
Oid relid;
- /* Replication slot name. */
- NameData slotname;
-
- /*
- * Decoded changes are stored here. Although we try to avoid excessive
- * batches, it can happen that the changes need to be stored to disk. The
- * tuplestore does this transparently.
- */
- Tuplestorestate *tstore;
-
- /* The current number of changes in tstore. */
- double nchanges;
-
- /*
- * Descriptor to store the ConcurrentChange structure serialized (bytea).
- * We can't store the tuple directly because tuplestore only supports
- * minimum tuple and we may need to transfer OID system column from the
- * output plugin. Also we need to transfer the change kind, so it's better
- * to put everything in the structure than to use 2 tuplestores "in
- * parallel".
- */
- TupleDesc tupdesc_change;
-
- /* Tuple descriptor needed to update indexes. */
+ /* Tuple descriptor of the relation being processed. */
TupleDesc tupdesc;
- /* Slot to retrieve data from tstore. */
- TupleTableSlot *tsslot;
-
- ResourceOwner resowner;
+ /* The current output file. */
+ BufFile *file;
} RepackDecodingState;
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
+extern bool IsRepackWorker(void);
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
@@ -136,6 +103,6 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
extern bool am_decoding_for_repack(void);
extern bool change_useless_for_repack(XLogRecordBuffer *buf);
-extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
- XLogRecPtr end_of_wal);
+
+extern void RepackWorkerMain(Datum main_arg);
#endif /* CLUSTER_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..a944ee0d211 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
PROCSIG_BARRIER, /* global barrier interrupt */
PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */
PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check
* PGPROC->pendingRecoveryConflicts for the
* reason */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 71a0802187e..b35d4e318fc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -499,7 +499,6 @@ CompressFileHandle
CompressionLocation
CompressorState
ComputeXidHorizonsResult
-ConcurrentChange
ConcurrentChangeKind
ConditionVariable
ConditionVariableMinimallyPadded
@@ -639,6 +638,8 @@ DeclareCursorStmt
DecodedBkpBlock
DecodedXLogRecord
DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
DefElem
DefElemAction
DefaultACLInfo
--
2.47.3
--=-=-=--
^ permalink raw reply [nested|flat] 28+ messages in thread
end of thread, other threads:[~2026-02-16 19:49 UTC | newest]
Thread overview: 28+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-08-28 05:12 [PATCH v23 5/5] Additional test for new GUC setting. Kyotaro Horiguchi <[email protected]>
2019-08-28 05:12 [PATCH 4/4] Additional test for new GUC setting. Kyotaro Horiguchi <[email protected]>
2019-08-28 05:12 [PATCH v21 5/5] Additional test for new GUC setting. Kyotaro Horiguchi <[email protected]>
2019-08-28 05:12 [PATCH v23 5/5] Additional test for new GUC setting. Kyotaro Horiguchi <[email protected]>
2023-12-06 10:42 Re: remaining sql/json patches Alvaro Herrera <[email protected]>
2023-12-06 14:02 ` Re: remaining sql/json patches Amit Langote <[email protected]>
2023-12-07 03:10 ` Re: remaining sql/json patches jian he <[email protected]>
2023-12-07 10:39 ` Re: remaining sql/json patches jian he <[email protected]>
2023-12-07 12:13 ` Re: remaining sql/json patches Amit Langote <[email protected]>
2023-12-07 08:25 ` Re: remaining sql/json patches Peter Eisentraut <[email protected]>
2025-12-09 18:44 [PATCH 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2025-12-09 18:44 [PATCH 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2025-12-13 18:27 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2025-12-13 18:27 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-08 16:47 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-08 16:47 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-12 16:30 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-12 16:30 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-22 08:23 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-22 08:23 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-27 10:48 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-27 10:48 [PATCH v33 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-27 10:48 [PATCH 5/6] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-01-27 10:48 [PATCH v33 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-02-16 19:49 [PATCH v35 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-02-16 19:49 [PATCH 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-02-16 19:49 [PATCH v35 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
2026-02-16 19:49 [PATCH 5/5] Use background worker to do logical decoding. Antonin Houska <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox