From 22972abac0363f594e63218a345089ade39f591c Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Fri, 19 Jan 2024 09:44:58 +0100 Subject: [PATCH 09/20] PREPARE LET support In current code base it requires just small changes in parser. It is nice to have to have possibility to use prepared LET statements mainly due reduction of security risks (SQL injection), and implementation is almost cheap. Explicit preparing is not necessity for support of LET statements in PL, because PL uses SPI for query execution, but it is nice to have this possibility (completenees, ...) --- doc/src/sgml/ref/prepare.sgml | 4 +- src/backend/parser/gram.y | 3 +- src/backend/parser/parse_cte.c | 7 ++ src/bin/psql/tab-complete.c | 4 +- .../regress/expected/session_variables.out | 81 ++++++++++++++++++- src/test/regress/sql/session_variables.sql | 57 +++++++++++++ 6 files changed, 149 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/ref/prepare.sgml b/doc/src/sgml/ref/prepare.sgml index 8ee9439f611..45d72b1d52b 100644 --- a/doc/src/sgml/ref/prepare.sgml +++ b/doc/src/sgml/ref/prepare.sgml @@ -116,8 +116,8 @@ PREPARE name [ ( Any SELECT, INSERT, UPDATE, - DELETE, MERGE, or VALUES - statement. + DELETE, MERGE, VALUES, + or LET statement. diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index fcacef276cf..c17a71f6656 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12163,7 +12163,8 @@ PreparableStmt: | InsertStmt | UpdateStmt | DeleteStmt - | MergeStmt /* by default all are $$=$1 */ + | MergeStmt + | LetStmt /* by default all are $$=$1 */ ; /***************************************************************************** diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c index de9ae9b4834..b4b9b5b61c8 100644 --- a/src/backend/parser/parse_cte.c +++ b/src/backend/parser/parse_cte.c @@ -126,6 +126,13 @@ transformWithClause(ParseState *pstate, WithClause *withClause) CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc); ListCell *rest; + /* LET is allowed by parser, but not supported. Reject for now */ + if (IsA(cte->ctequery, LetStmt)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("LET not supported in WITH query"), + parser_errposition(pstate, cte->location)); + for_each_cell(rest, withClause->ctes, lnext(withClause->ctes, lc)) { CommonTableExpr *cte2 = (CommonTableExpr *) lfirst(rest); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index f4a3af26ab2..e455890217e 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -4288,9 +4288,9 @@ psql_completion(const char *text, int start, int end) else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); -/* LET */ +/* LET, EXPLAIN LET, PREPARE LET */ /* If prev. word is LET suggest a list of variables */ - else if (Matches("LET")) + else if (TailMatches("LET")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); /* Complete LET with "=" */ else if (TailMatches("LET", MatchAny)) diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 11eaea669db..358502fa31c 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -428,9 +428,9 @@ DROP VARIABLE var1, var2; -- the LET statement should be disallowed in CTE CREATE VARIABLE var1 AS int; WITH x AS (LET var1 = 100) SELECT * FROM x; -ERROR: syntax error at or near "LET" +ERROR: LET not supported in WITH query LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x; - ^ + ^ -- should be ok LET var1 = generate_series(1, 1); -- should fail @@ -1308,5 +1308,82 @@ SELECT var1; 10 (1 row) +SET plan_cache_mode TO force_generic_plan; +PREPARE p1 AS LET var1 = (SELECT count(*) FROM var_tab_test_table); +LET var1 = NULL; +EXPLAIN (COSTS OFF) EXECUTE p1; + QUERY PLAN +---------------------------------------------- + SET SESSION VARIABLE + Result + InitPlan 1 + -> Aggregate + -> Seq Scan on var_tab_test_table +(5 rows) + +-- should be NULL +SELECT var1; + var1 +------ + +(1 row) + +EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) EXECUTE p1; + QUERY PLAN +----------------------------------------------------------------------- + SET SESSION VARIABLE + Result (actual rows=1 loops=1) + InitPlan 1 + -> Aggregate (actual rows=1 loops=1) + -> Seq Scan on var_tab_test_table (actual rows=10 loops=1) +(5 rows) + +-- should be 10 +SELECT var1; + var1 +------ + 10 +(1 row) + +SET plan_cache_mode TO DEFAULT; +DEALLOCATE p1; DROP VARIABLE var1; DROP TABLE var_tab_test_table; +CREATE VARIABLE var1 numeric; +SET plan_cache_mode TO force_generic_plan; +PREPARE p1(numeric) AS LET var1 = $1; +PREPARE p2 AS SELECT var1; +EXECUTE p1(pi() + 100); +EXECUTE p2; + var1 +----------------- + 103.14159265359 +(1 row) + +-- prepared plan cache invalidation test +DROP VARIABLE var1; +CREATE VARIABLE var1 numeric; +-- should be NULL +EXECUTE p2; + var1 +------ + +(1 row) + +DEALLOCATE p1; +DEALLOCATE p2; +DROP VARIABLE var1; +SET plan_cache_mode TO force_generic_plan; +CREATE VARIABLE var1 numeric[]; +PREPARE p1(int, numeric) AS LET var1[$1] = $2; +LET var1 = '{}'::numeric[]; +EXECUTE p1(1, 10.2); +EXECUTE p1(2, 10.3); +SELECT var1; + var1 +------------- + {10.2,10.3} +(1 row) + +DEALLOCATE p1; +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index fa0a57fc77e..3436b57e8c7 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -880,5 +880,62 @@ EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(* -- should be 10 SELECT var1; +SET plan_cache_mode TO force_generic_plan; + +PREPARE p1 AS LET var1 = (SELECT count(*) FROM var_tab_test_table); + +LET var1 = NULL; + +EXPLAIN (COSTS OFF) EXECUTE p1; + +-- should be NULL +SELECT var1; + +EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) EXECUTE p1; + +-- should be 10 +SELECT var1; + +SET plan_cache_mode TO DEFAULT; + +DEALLOCATE p1; + DROP VARIABLE var1; DROP TABLE var_tab_test_table; + +CREATE VARIABLE var1 numeric; + +SET plan_cache_mode TO force_generic_plan; + +PREPARE p1(numeric) AS LET var1 = $1; +PREPARE p2 AS SELECT var1; + +EXECUTE p1(pi() + 100); +EXECUTE p2; + +-- prepared plan cache invalidation test +DROP VARIABLE var1; +CREATE VARIABLE var1 numeric; + +-- should be NULL +EXECUTE p2; + +DEALLOCATE p1; +DEALLOCATE p2; + +DROP VARIABLE var1; + +SET plan_cache_mode TO force_generic_plan; + +CREATE VARIABLE var1 numeric[]; + +PREPARE p1(int, numeric) AS LET var1[$1] = $2; + +LET var1 = '{}'::numeric[]; +EXECUTE p1(1, 10.2); +EXECUTE p1(2, 10.3); + +SELECT var1; + +DEALLOCATE p1; +DROP VARIABLE var1; -- 2.45.2