Compare commits

...

4 Commits

Author SHA1 Message Date
Jeff Davis
5b5318c387 Fix buildfarm error from commit 5c31669058.
Skip test when not using unix domain sockets.

Discussion: https://postgr.es/m/CALDaNm29-8OozsBWo9H6DN_Tb_3yA1QjRJput-KhaN8ncDJtJA@mail.gmail.com
Backpatch-through: 16
2024-01-18 15:00:15 -08:00
Tom Lane
00f941356e Fix plpgsql to allow new-style SQL CREATE FUNCTION as a SQL command.
plpgsql fails on new-style CREATE FUNCTION/PROCEDURE commands within
a routine or DO block, because make_execsql_stmt believes that a
semicolon token always terminates a SQL command.  Now, that's actually
been wrong since the day it was written, because CREATE RULE has long
allowed multiple rule actions separated by semicolons.  But there are
few enough people using multi-action rules that there was never an
attempt to fix it.  New-style SQL functions, though, are popular.

psql has this same problem of "does this semicolon really terminate
the command?".  It deals with CREATE RULE by counting parenthesis
nesting depth: a semicolon within parens doesn't end a command.
Commits e717a9a18 and 029c5ac03 created a similar heuristic to count
matching BEGIN/END pairs (but only within CREATEs, so as not to be
fooled by plain BEGIN).  That's survived several releases now without
trouble reports, so let's just absorb those heuristics into plpgsql.

Per report from Samuel Dussault.  Back-patch to v14 where new-style
SQL function syntax came in.

Discussion: https://postgr.es/m/YT2PR01MB88552C3E9AD40A6C038774A781722@YT2PR01MB8855.CANPRD01.PROD.OUTLOOK.COM
2024-01-18 16:10:57 -05:00
Michael Paquier
c030e263e7 Improve handling of dropped partitioned indexes for REINDEX INDEX
A REINDEX INDEX done on a partitioned index builds a list of the indexes
to work on before processing its partitions in individual transactions.
When combined with a DROP of the partitioned index, there was a window
where it was possible to see some unexpected "could not open relation
with OID", synonym of relation lookup error.  The code was robust enough
to handle the case where the parent relation is missing, but not the
case where an index would be gone missing.

This is similar to 1d65416661bb.

Support for REINDEX on partitioned relations has been introduced in
a6642b3ae060, so backpatch down to 14.

Author: Fei Changhong
Discussion: https://postgr.es/m/tencent_6A52106095ACDE55333E3AD33F304C0C3909@qq.com
Backpatch-through: 14
2024-01-18 16:31:38 +09:00
Michael Paquier
7ce65c6f72 Add try_index_open(), conditional variant of index_open()
try_index_open() is able to open an index if its relkind fits, except
that it would return NULL instead of generated an error if the relation
does not exist.  This new routine will be used by an upcoming patch to
make REINDEX on partitioned relations more robust when an index in a
partition tree is dropped.

Extracted from a larger patch by the same author.

Author: Fei Changhong
Discussion: https://postgr.es/m/tencent_6A52106095ACDE55333E3AD33F304C0C3909@qq.com
Backpatch-through: 14
2024-01-18 15:04:31 +09:00
9 changed files with 269 additions and 92 deletions

View File

@ -107,6 +107,7 @@ do { \
static IndexScanDesc index_beginscan_internal(Relation indexRelation,
int nkeys, int norderbys, Snapshot snapshot,
ParallelIndexScanDesc pscan, bool temp_snap);
static inline void validate_relation_kind(Relation r);
/* ----------------------------------------------------------------
@ -135,12 +136,30 @@ index_open(Oid relationId, LOCKMODE lockmode)
r = relation_open(relationId, lockmode);
if (r->rd_rel->relkind != RELKIND_INDEX &&
r->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index",
RelationGetRelationName(r))));
validate_relation_kind(r);
return r;
}
/* ----------------
* try_index_open - open a index relation by relation OID
*
* Same as index_open, except return NULL instead of failing
* if the relation does not exist.
* ----------------
*/
Relation
try_index_open(Oid relationId, LOCKMODE lockmode)
{
Relation r;
r = try_relation_open(relationId, lockmode);
/* leave if index does not exist */
if (!r)
return NULL;
validate_relation_kind(r);
return r;
}
@ -168,6 +187,24 @@ index_close(Relation relation, LOCKMODE lockmode)
UnlockRelationId(&relid, lockmode);
}
/* ----------------
* validate_relation_kind - check the relation's kind
*
* Make sure relkind is an index or a partitioned index.
* ----------------
*/
static inline void
validate_relation_kind(Relation r)
{
if (r->rd_rel->relkind != RELKIND_INDEX &&
r->rd_rel->relkind != RELKIND_PARTITIONED_INDEX)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not an index",
RelationGetRelationName(r))));
}
/* ----------------
* index_insert - insert an index tuple into a relation
* ----------------

View File

@ -3633,7 +3633,24 @@ reindex_index(Oid indexId, bool skip_constraint_checks, char persistence,
* Open the target index relation and get an exclusive lock on it, to
* ensure that no one else is touching this particular index.
*/
iRel = index_open(indexId, AccessExclusiveLock);
if ((params->options & REINDEXOPT_MISSING_OK) != 0)
iRel = try_index_open(indexId, AccessExclusiveLock);
else
iRel = index_open(indexId, AccessExclusiveLock);
/* if index relation is gone, leave */
if (!iRel)
{
/* Roll back any GUC changes */
AtEOXact_GUC(false, save_nestlevel);
/* Restore userid and security context */
SetUserIdAndSecContext(save_userid, save_sec_context);
/* Close parent heap relation, but keep locks */
table_close(heapRelation, NoLock);
return;
}
if (progress)
pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,

View File

@ -139,6 +139,7 @@ typedef struct IndexOrderByDistance
#define IndexScanIsValid(scan) PointerIsValid(scan)
extern Relation index_open(Oid relationId, LOCKMODE lockmode);
extern Relation try_index_open(Oid relationId, LOCKMODE lockmode);
extern void index_close(Relation relation, LOCKMODE lockmode);
extern bool index_insert(Relation indexRelation,

View File

@ -32,8 +32,9 @@ DATA = plpgsql.control plpgsql--1.0.sql
REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_array plpgsql_call plpgsql_control plpgsql_copy plpgsql_domain \
plpgsql_record plpgsql_cache plpgsql_simple plpgsql_transaction \
REGRESS = plpgsql_array plpgsql_cache plpgsql_call plpgsql_control \
plpgsql_copy plpgsql_domain plpgsql_misc \
plpgsql_record plpgsql_simple plpgsql_transaction \
plpgsql_trap plpgsql_trigger plpgsql_varprops
# where to find gen_keywordlist.pl and subsidiary files

View File

@ -0,0 +1,31 @@
--
-- Miscellaneous topics
--
-- Verify that we can parse new-style CREATE FUNCTION/PROCEDURE
do
$$
declare procedure int; -- check we still recognize non-keywords as vars
begin
create function test1() returns int
begin atomic
select 2 + 2;
end;
create or replace procedure test2(x int)
begin atomic
select x + 2;
end;
end
$$;
\sf test1
CREATE OR REPLACE FUNCTION public.test1()
RETURNS integer
LANGUAGE sql
BEGIN ATOMIC
SELECT (2 + 2);
END
\sf test2
CREATE OR REPLACE PROCEDURE public.test2(IN x integer)
LANGUAGE sql
BEGIN ATOMIC
SELECT (x + 2);
END

View File

@ -76,12 +76,13 @@ tests += {
'regress': {
'sql': [
'plpgsql_array',
'plpgsql_cache',
'plpgsql_call',
'plpgsql_control',
'plpgsql_copy',
'plpgsql_domain',
'plpgsql_misc',
'plpgsql_record',
'plpgsql_cache',
'plpgsql_simple',
'plpgsql_transaction',
'plpgsql_trap',

View File

@ -76,7 +76,8 @@ static PLpgSQL_expr *read_sql_expression2(int until, int until2,
int *endtoken);
static PLpgSQL_expr *read_sql_stmt(void);
static PLpgSQL_type *read_datatype(int tok);
static PLpgSQL_stmt *make_execsql_stmt(int firsttoken, int location);
static PLpgSQL_stmt *make_execsql_stmt(int firsttoken, int location,
PLword *word);
static PLpgSQL_stmt_fetch *read_fetch_direction(void);
static void complete_direction(PLpgSQL_stmt_fetch *fetch,
bool *check_FROM);
@ -1971,15 +1972,15 @@ loop_body : proc_sect K_END K_LOOP opt_label ';'
*/
stmt_execsql : K_IMPORT
{
$$ = make_execsql_stmt(K_IMPORT, @1);
$$ = make_execsql_stmt(K_IMPORT, @1, NULL);
}
| K_INSERT
{
$$ = make_execsql_stmt(K_INSERT, @1);
$$ = make_execsql_stmt(K_INSERT, @1, NULL);
}
| K_MERGE
{
$$ = make_execsql_stmt(K_MERGE, @1);
$$ = make_execsql_stmt(K_MERGE, @1, NULL);
}
| T_WORD
{
@ -1990,7 +1991,7 @@ stmt_execsql : K_IMPORT
if (tok == '=' || tok == COLON_EQUALS ||
tok == '[' || tok == '.')
word_is_not_variable(&($1), @1);
$$ = make_execsql_stmt(T_WORD, @1);
$$ = make_execsql_stmt(T_WORD, @1, &($1));
}
| T_CWORD
{
@ -2001,7 +2002,7 @@ stmt_execsql : K_IMPORT
if (tok == '=' || tok == COLON_EQUALS ||
tok == '[' || tok == '.')
cword_is_not_variable(&($1), @1);
$$ = make_execsql_stmt(T_CWORD, @1);
$$ = make_execsql_stmt(T_CWORD, @1, NULL);
}
;
@ -2919,8 +2920,13 @@ read_datatype(int tok)
return result;
}
/*
* Read a generic SQL statement. We have already read its first token;
* firsttoken is that token's code and location its starting location.
* If firsttoken == T_WORD, pass its yylval value as "word", else pass NULL.
*/
static PLpgSQL_stmt *
make_execsql_stmt(int firsttoken, int location)
make_execsql_stmt(int firsttoken, int location, PLword *word)
{
StringInfoData ds;
IdentifierLookup save_IdentifierLookup;
@ -2933,9 +2939,16 @@ make_execsql_stmt(int firsttoken, int location)
bool have_strict = false;
int into_start_loc = -1;
int into_end_loc = -1;
int paren_depth = 0;
int begin_depth = 0;
bool in_routine_definition = false;
int token_count = 0;
char tokens[4]; /* records the first few tokens */
initStringInfo(&ds);
memset(tokens, 0, sizeof(tokens));
/* special lookup mode for identifiers within the SQL text */
save_IdentifierLookup = plpgsql_IdentifierLookup;
plpgsql_IdentifierLookup = IDENTIFIER_LOOKUP_EXPR;
@ -2944,6 +2957,12 @@ make_execsql_stmt(int firsttoken, int location)
* Scan to the end of the SQL command. Identify any INTO-variables
* clause lurking within it, and parse that via read_into_target().
*
* The end of the statement is defined by a semicolon ... except that
* semicolons within parentheses or BEGIN/END blocks don't terminate a
* statement. We follow psql's lead in not recognizing BEGIN/END except
* after CREATE [OR REPLACE] {FUNCTION|PROCEDURE}. END can also appear
* within a CASE construct, so we treat CASE/END like BEGIN/END.
*
* Because INTO is sometimes used in the main SQL grammar, we have to be
* careful not to take any such usage of INTO as a PL/pgSQL INTO clause.
* There are currently three such cases:
@ -2969,13 +2988,50 @@ make_execsql_stmt(int firsttoken, int location)
* break this logic again ... beware!
*/
tok = firsttoken;
if (tok == T_WORD && strcmp(word->ident, "create") == 0)
tokens[token_count] = 'c';
token_count++;
for (;;)
{
prev_tok = tok;
tok = yylex();
if (have_into && into_end_loc < 0)
into_end_loc = yylloc; /* token after the INTO part */
if (tok == ';')
/* Detect CREATE [OR REPLACE] {FUNCTION|PROCEDURE} */
if (tokens[0] == 'c' && token_count < sizeof(tokens))
{
if (tok == K_OR)
tokens[token_count] = 'o';
else if (tok == T_WORD &&
strcmp(yylval.word.ident, "replace") == 0)
tokens[token_count] = 'r';
else if (tok == T_WORD &&
strcmp(yylval.word.ident, "function") == 0)
tokens[token_count] = 'f';
else if (tok == T_WORD &&
strcmp(yylval.word.ident, "procedure") == 0)
tokens[token_count] = 'f'; /* treat same as "function" */
if (tokens[1] == 'f' ||
(tokens[1] == 'o' && tokens[2] == 'r' && tokens[3] == 'f'))
in_routine_definition = true;
token_count++;
}
/* Track paren nesting (needed for CREATE RULE syntax) */
if (tok == '(')
paren_depth++;
else if (tok == ')' && paren_depth > 0)
paren_depth--;
/* We need track BEGIN/END nesting only in a routine definition */
if (in_routine_definition && paren_depth == 0)
{
if (tok == K_BEGIN || tok == K_CASE)
begin_depth++;
else if (tok == K_END && begin_depth > 0)
begin_depth--;
}
/* Command-ending semicolon? */
if (tok == ';' && paren_depth == 0 && begin_depth == 0)
break;
if (tok == 0)
yyerror("unexpected end of function definition");

View File

@ -0,0 +1,22 @@
--
-- Miscellaneous topics
--
-- Verify that we can parse new-style CREATE FUNCTION/PROCEDURE
do
$$
declare procedure int; -- check we still recognize non-keywords as vars
begin
create function test1() returns int
begin atomic
select 2 + 2;
end;
create or replace procedure test2(x int)
begin atomic
select x + 2;
end;
end
$$;
\sf test1
\sf test2

View File

@ -5,6 +5,7 @@
use strict;
use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my ($node_publisher, $node_subscriber, $publisher_connstr, $result, $offset);
@ -306,81 +307,91 @@ expect_replication("alice.unpartitioned", 3, 17, 21,
# If the subscription connection requires a password ('password_required'
# is true) then a non-superuser must specify that password in the connection
# string.
$ENV{"PGPASSWORD"} = 'secret';
my $node_publisher1 = PostgreSQL::Test::Cluster->new('publisher1');
my $node_subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
$node_publisher1->init(allows_streaming => 'logical');
$node_subscriber1->init;
$node_publisher1->start;
$node_subscriber1->start;
my $publisher_connstr1 =
$node_publisher1->connstr . ' user=regress_test_user dbname=postgres';
my $publisher_connstr2 =
$node_publisher1->connstr
. ' user=regress_test_user dbname=postgres password=secret';
for my $node ($node_publisher1, $node_subscriber1)
SKIP:
{
$node->safe_psql(
skip
"subscription password_required test cannot run without Unix-domain sockets",
3
unless $use_unix_sockets;
my $node_publisher1 = PostgreSQL::Test::Cluster->new('publisher1');
my $node_subscriber1 = PostgreSQL::Test::Cluster->new('subscriber1');
$node_publisher1->init(allows_streaming => 'logical');
$node_subscriber1->init;
$node_publisher1->start;
$node_subscriber1->start;
my $publisher_connstr1 =
$node_publisher1->connstr . ' user=regress_test_user dbname=postgres';
my $publisher_connstr2 =
$node_publisher1->connstr
. ' user=regress_test_user dbname=postgres password=secret';
for my $node ($node_publisher1, $node_subscriber1)
{
$node->safe_psql(
'postgres', qq(
CREATE ROLE regress_test_user PASSWORD 'secret' LOGIN REPLICATION;
GRANT CREATE ON DATABASE postgres TO regress_test_user;
GRANT PG_CREATE_SUBSCRIPTION TO regress_test_user;
));
}
$node_publisher1->safe_psql(
'postgres', qq(
CREATE ROLE regress_test_user PASSWORD 'secret' LOGIN REPLICATION;
GRANT CREATE ON DATABASE postgres TO regress_test_user;
GRANT PG_CREATE_SUBSCRIPTION TO regress_test_user;
));
SET SESSION AUTHORIZATION regress_test_user;
CREATE PUBLICATION regress_test_pub;
));
$node_subscriber1->safe_psql(
'postgres', qq(
CREATE SUBSCRIPTION regress_test_sub CONNECTION '$publisher_connstr1' PUBLICATION regress_test_pub;
));
# Wait for initial sync to finish
$node_subscriber1->wait_for_subscription_sync($node_publisher1,
'regress_test_sub');
my $save_pgpassword = $ENV{"PGPASSWORD"};
$ENV{"PGPASSWORD"} = 'secret';
# Setup pg_hba configuration so that logical replication connection without
# password is not allowed.
unlink($node_publisher1->data_dir . '/pg_hba.conf');
$node_publisher1->append_conf('pg_hba.conf',
qq{local all regress_test_user md5});
$node_publisher1->reload;
# Change the subscription owner to a non-superuser
$node_subscriber1->safe_psql(
'postgres', qq(
ALTER SUBSCRIPTION regress_test_sub OWNER TO regress_test_user;
));
# Non-superuser must specify password in the connection string
my ($ret, $stdout, $stderr) = $node_subscriber1->psql(
'postgres', qq(
SET SESSION AUTHORIZATION regress_test_user;
ALTER SUBSCRIPTION regress_test_sub REFRESH PUBLICATION;
));
isnt($ret, 0,
"non zero exit for subscription whose owner is a non-superuser must specify password parameter of the connection string"
);
ok( $stderr =~
m/DETAIL: Non-superusers must provide a password in the connection string./,
'subscription whose owner is a non-superuser must specify password parameter of the connection string'
);
$ENV{"PGPASSWORD"} = $save_pgpassword;
# It should succeed after including the password parameter of the connection
# string.
($ret, $stdout, $stderr) = $node_subscriber1->psql(
'postgres', qq(
SET SESSION AUTHORIZATION regress_test_user;
ALTER SUBSCRIPTION regress_test_sub CONNECTION '$publisher_connstr2';
ALTER SUBSCRIPTION regress_test_sub REFRESH PUBLICATION;
));
is($ret, 0,
"Non-superuser will be able to refresh the publication after specifying the password parameter of the connection string"
);
}
$node_publisher1->safe_psql(
'postgres', qq(
SET SESSION AUTHORIZATION regress_test_user;
CREATE PUBLICATION regress_test_pub;
));
$node_subscriber1->safe_psql(
'postgres', qq(
CREATE SUBSCRIPTION regress_test_sub CONNECTION '$publisher_connstr1' PUBLICATION regress_test_pub;
));
# Wait for initial sync to finish
$node_subscriber1->wait_for_subscription_sync($node_publisher1,
'regress_test_sub');
# Setup pg_hba configuration so that logical replication connection without
# password is not allowed.
unlink($node_publisher1->data_dir . '/pg_hba.conf');
$node_publisher1->append_conf('pg_hba.conf',
qq{local all regress_test_user md5});
$node_publisher1->reload;
# Change the subscription owner to a non-superuser
$node_subscriber1->safe_psql(
'postgres', qq(
ALTER SUBSCRIPTION regress_test_sub OWNER TO regress_test_user;
));
# Non-superuser must specify password in the connection string
my ($ret, $stdout, $stderr) = $node_subscriber1->psql(
'postgres', qq(
SET SESSION AUTHORIZATION regress_test_user;
ALTER SUBSCRIPTION regress_test_sub REFRESH PUBLICATION;
));
isnt($ret, 0,
"non zero exit for subscription whose owner is a non-superuser must specify password parameter of the connection string"
);
ok( $stderr =~ m/DETAIL: Non-superusers must provide a password in the connection string./,
'subscription whose owner is a non-superuser must specify password parameter of the connection string'
);
delete $ENV{"PGPASSWORD"};
# It should succeed after including the password parameter of the connection
# string.
($ret, $stdout, $stderr) = $node_subscriber1->psql(
'postgres', qq(
SET SESSION AUTHORIZATION regress_test_user;
ALTER SUBSCRIPTION regress_test_sub CONNECTION '$publisher_connstr2';
ALTER SUBSCRIPTION regress_test_sub REFRESH PUBLICATION;
));
is($ret, 0,
"Non-superuser will be able to refresh the publication after specifying the password parameter of the connection string"
);
done_testing();