1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00
Files
postgres/contrib/pg_plan_advice/pgpa_parser.y
Robert Haas 63920eb099 pg_plan_advice: Disallow partition name without partition schema.
Up until now, pg_plan_advice has had a feature that allows an advice
target to mention a partition name but omit the partition schema;
that is, something like SEQ_SCAN(foo/bar) forces a sequential scan on
every child of table foo whose partition name is bar, regardless of
the schema in which bar appears. However, that feature turns out to
have a nasty design flaw: while advice enforcement handles this case
just fine, the advice feedback code doesn't know about it and will
mark such advice as "matched, failed" even when everything worked
perfectly. Unfortunately, there seems to be no simple code fix for
this problem.

Since the release of PostgreSQL 19 is imminent, take the conservative
course and revert this feature of pg_plan_advice. In other words,
require the partition schema whenever the partition name is present.
You must now write e.g. SEQ_SCAN(foo/public.bar) rather than just
SEQ_SCAN(foo/bar). This doesn't affect any cases where automatically
generated advice is supplied, since generated advice has always
included the partition schema anyway. Manually written advice will
have to conform to the new, stricter rule. For a later release, we
can consider whether to again relax this restriction in some way,
but now is not the time to design new things.

Reported-by: Noah Misch <noah@leadboat.com>
Backpatch-through: 19
Discussion: https://postgr.es/m/CA+TgmoYmXy-jiP5qDhqNEiYFEBzQsArO6O2d9E8szNZqi1bePQ@mail.gmail.com
2026-09-16 21:03:58 -04:00

308 lines
6.8 KiB
Plaintext

%{
/*
* Parser for plan advice
*
* Copyright (c) 2000-2026, PostgreSQL Global Development Group
*
* contrib/pg_plan_advice/pgpa_parser.y
*/
#include "postgres.h"
#include <float.h>
#include <math.h>
#include "fmgr.h"
#include "nodes/miscnodes.h"
#include "utils/builtins.h"
#include "utils/float.h"
#include "pgpa_ast.h"
#include "pgpa_parser.h"
/*
* Bison doesn't allocate anything that needs to live across parser calls,
* so we can easily have it use palloc instead of malloc. This prevents
* memory leaks if we error out during parsing.
*/
#define YYMALLOC palloc
#define YYFREE pfree
%}
/* BISON Declarations */
%parse-param {List **result}
%parse-param {char **parse_error_msg_p}
%parse-param {yyscan_t yyscanner}
%lex-param {List **result}
%lex-param {char **parse_error_msg_p}
%lex-param {yyscan_t yyscanner}
%pure-parser
%expect 0
%name-prefix="pgpa_yy"
%union
{
char *str;
int integer;
List *list;
pgpa_advice_item *item;
pgpa_advice_target *target;
pgpa_index_target *itarget;
}
%token <str> TOK_IDENT TOK_TAG_JOIN_ORDER TOK_TAG_INDEX
%token <str> TOK_TAG_SIMPLE TOK_TAG_GENERIC
%token <integer> TOK_INTEGER
%type <integer> opt_ri_occurrence
%type <item> advice_item
%type <list> advice_item_list generic_target_list
%type <list> index_target_list join_order_target_list
%type <list> opt_partition simple_target_list
%type <str> identifier opt_plan_name
%type <target> generic_sublist join_order_sublist
%type <target> relation_identifier
%type <itarget> index_name
%start parse_toplevel
/* Grammar follows */
%%
parse_toplevel: advice_item_list
{
(void) yynerrs; /* suppress compiler warning */
*result = $1;
}
;
advice_item_list: advice_item_list advice_item
{ $$ = lappend($1, $2); }
|
{ $$ = NIL; }
;
advice_item: TOK_TAG_JOIN_ORDER '(' join_order_target_list ')'
{
$$ = palloc0_object(pgpa_advice_item);
$$->tag = PGPA_TAG_JOIN_ORDER;
$$->targets = $3;
if ($3 == NIL)
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"JOIN_ORDER must have at least one target");
}
| TOK_TAG_INDEX '(' index_target_list ')'
{
$$ = palloc0_object(pgpa_advice_item);
if (strcmp($1, "index_only_scan") == 0)
$$->tag = PGPA_TAG_INDEX_ONLY_SCAN;
else if (strcmp($1, "index_scan") == 0)
$$->tag = PGPA_TAG_INDEX_SCAN;
else
elog(ERROR, "tag parsing failed: %s", $1);
$$->targets = $3;
}
| TOK_TAG_SIMPLE '(' simple_target_list ')'
{
$$ = palloc0_object(pgpa_advice_item);
if (strcmp($1, "bitmap_heap_scan") == 0)
$$->tag = PGPA_TAG_BITMAP_HEAP_SCAN;
else if (strcmp($1, "do_not_scan") == 0)
$$->tag = PGPA_TAG_DO_NOT_SCAN;
else if (strcmp($1, "no_gather") == 0)
$$->tag = PGPA_TAG_NO_GATHER;
else if (strcmp($1, "seq_scan") == 0)
$$->tag = PGPA_TAG_SEQ_SCAN;
else if (strcmp($1, "tid_scan") == 0)
$$->tag = PGPA_TAG_TID_SCAN;
else
elog(ERROR, "tag parsing failed: %s", $1);
$$->targets = $3;
}
| TOK_TAG_GENERIC '(' generic_target_list ')'
{
bool fail;
$$ = palloc0_object(pgpa_advice_item);
$$->tag = pgpa_parse_advice_tag($1, &fail);
if (fail)
{
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"unrecognized advice tag");
}
if ($$->tag == PGPA_TAG_FOREIGN_JOIN)
{
foreach_ptr(pgpa_advice_target, target, $3)
{
if (target->ttype == PGPA_TARGET_IDENTIFIER ||
list_length(target->children) < 2)
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"FOREIGN_JOIN targets must contain more than one relation identifier");
}
}
$$->targets = $3;
}
;
relation_identifier: identifier opt_ri_occurrence opt_partition opt_plan_name
{
$$ = palloc0_object(pgpa_advice_target);
$$->ttype = PGPA_TARGET_IDENTIFIER;
$$->rid.alias_name = $1;
$$->rid.occurrence = $2;
if ($3 != NIL)
{
Assert(list_length($3) == 2);
$$->rid.partnsp = linitial($3);
$$->rid.partrel = lsecond($3);
}
$$->rid.plan_name = $4;
}
;
index_name: identifier
{
$$ = palloc0_object(pgpa_index_target);
$$->indname = $1;
}
| identifier '.' identifier
{
$$ = palloc0_object(pgpa_index_target);
$$->indnamespace = $1;
$$->indname = $3;
}
;
opt_ri_occurrence:
'#' TOK_INTEGER
{
if ($2 <= 0)
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"only positive occurrence numbers are permitted");
$$ = $2;
}
|
{
/* The default occurrence number is 1. */
$$ = 1;
}
;
identifier: TOK_IDENT
| TOK_TAG_JOIN_ORDER
| TOK_TAG_INDEX
| TOK_TAG_SIMPLE
| TOK_TAG_GENERIC
;
/*
* The partition name must always be schema-qualified. Otherwise, a relation
* identifier could refer to more than one partition of the same partitioned
* table, and relation identifiers are required to be unique.
*/
opt_partition:
'/' identifier '.' identifier
{ $$ = list_make2($2, $4); }
|
{ $$ = NIL; }
;
opt_plan_name:
'@' identifier
{ $$ = $2; }
|
{ $$ = NULL; }
;
generic_target_list: generic_target_list relation_identifier
{ $$ = lappend($1, $2); }
| generic_target_list generic_sublist
{ $$ = lappend($1, $2); }
|
{ $$ = NIL; }
;
generic_sublist: '(' simple_target_list ')'
{
$$ = palloc0_object(pgpa_advice_target);
$$->ttype = PGPA_TARGET_ORDERED_LIST;
$$->children = $2;
}
;
index_target_list:
index_target_list relation_identifier index_name
{
$2->itarget = $3;
$$ = lappend($1, $2);
}
|
{ $$ = NIL; }
;
join_order_target_list: join_order_target_list relation_identifier
{ $$ = lappend($1, $2); }
| join_order_target_list join_order_sublist
{ $$ = lappend($1, $2); }
|
{ $$ = NIL; }
;
join_order_sublist:
'(' join_order_target_list ')'
{
$$ = palloc0_object(pgpa_advice_target);
$$->ttype = PGPA_TARGET_ORDERED_LIST;
$$->children = $2;
if ($2 == NIL)
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"JOIN_ORDER targets must contain at least one relation identifier");
}
| '{' simple_target_list '}'
{
$$ = palloc0_object(pgpa_advice_target);
$$->ttype = PGPA_TARGET_UNORDERED_LIST;
$$->children = $2;
if ($2 == NIL)
pgpa_yyerror(result, parse_error_msg_p, yyscanner,
"JOIN_ORDER targets must contain at least one relation identifier");
}
;
simple_target_list: simple_target_list relation_identifier
{ $$ = lappend($1, $2); }
|
{ $$ = NIL; }
;
%%
/*
* Parse an advice_string and return the resulting list of pgpa_advice_item
* objects. If a parse error occurs, instead return NULL.
*
* If the return value is NULL, *error_p will be set to the error message;
* otherwise, *error_p will be set to NULL.
*/
List *
pgpa_parse(const char *advice_string, char **error_p)
{
yyscan_t scanner;
List *result;
char *error = NULL;
pgpa_scanner_init(advice_string, &scanner);
pgpa_yyparse(&result, &error, scanner);
pgpa_scanner_finish(scanner);
if (error != NULL)
{
*error_p = error;
return NULL;
}
*error_p = NULL;
return result;
}