diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2025-04-02 14:05:50 -0400 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2025-04-02 14:06:02 -0400 |
commit | 0dca5d68d7bebf2c1036fd84875533afef6df992 (patch) | |
tree | e9f713a5387a9782a8c2dddc54b461112f112ef0 /src/test | |
parent | e9e7b66044c9e3dfa76fd1599d5703acd3e4a3f5 (diff) | |
download | postgresql-0dca5d68d7bebf2c1036fd84875533afef6df992.tar.gz postgresql-0dca5d68d7bebf2c1036fd84875533afef6df992.zip |
Change SQL-language functions to use the plan cache.
In the historical implementation of SQL functions (if they don't get
inlined), we built plans for all the contained queries at first call
within an outer query, and then re-used those plans for the duration
of the outer query, and then forgot everything. This was not ideal,
not least because the plans could not be customized to specific values
of the function's parameters. Our plancache infrastructure seems
mature enough to be used here. That will solve both the problem with
not being able to build custom plans and the problem with not being
able to share work across successive outer queries.
Aside from those performance concerns, this change fixes a
longstanding bugaboo with SQL functions: you could not write DDL that
would affect later statements in the same function. That's mostly
still true with new-style SQL functions, since the results of parse
analysis are baked into the stored query trees (and protected by
dependency records). But for old-style SQL functions, it will now
work much as it does with PL/pgSQL functions, because we delay parse
analysis and planning of each query until we're ready to run it.
Some edge cases that require replanning are now handled better too;
see for example the new rowsecurity test, where we now detect an RLS
context change that was previously missed.
One other edge-case change that might be worthy of a release note
is that we now insist that a SQL function's result be generated
by the physically-last query within it. Previously, if the last
original query was deleted by a DO INSTEAD NOTHING rule, we'd be
willing to take the result from the preceding query instead.
This behavior was undocumented except in source-code comments,
and it seems hard to believe that anyone's relying on it.
Along the way to this feature, we needed a few infrastructure changes:
* The plancache can now take either a raw parse tree or an
analyzed-but-not-rewritten Query as the starting point for a
CachedPlanSource. If given a Query, it is caller's responsibility
that nothing will happen to invalidate that form of the query.
We use this for new-style SQL functions, where what's in pg_proc is
serialized Query(s) and we trust the dependency mechanism to disallow
DDL that would break those.
* The plancache now offers a way to invoke a post-rewrite callback
to examine/modify the rewritten parse tree when it is rebuilding
the parse trees after a cache invalidation. We need this because
SQL functions sometimes adjust the parse tree to make its output
exactly match the declared result type; if the plan gets rebuilt,
that has to be re-done.
* There is a new backend module utils/cache/funccache.c that
abstracts the idea of caching data about a specific function
usage (a particular function and set of input data types).
The code in it is moved almost verbatim from PL/pgSQL, which
has done that for a long time. We use that logic now for
SQL-language functions too, and maybe other PLs will have use
for it in the future.
Author: Alexander Pyhalov <a.pyhalov@postgrespro.ru>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com>
Discussion: https://postgr.es/m/8216639.NyiUUSuA9g@aivenlaptop
Diffstat (limited to 'src/test')
-rw-r--r-- | src/test/modules/test_extensions/expected/test_extensions.out | 2 | ||||
-rw-r--r-- | src/test/regress/expected/create_function_sql.out | 57 | ||||
-rw-r--r-- | src/test/regress/expected/rangefuncs.out | 2 | ||||
-rw-r--r-- | src/test/regress/expected/rowsecurity.out | 51 | ||||
-rw-r--r-- | src/test/regress/sql/create_function_sql.sql | 34 | ||||
-rw-r--r-- | src/test/regress/sql/rowsecurity.sql | 44 |
6 files changed, 187 insertions, 3 deletions
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index d5388a1fecf..72bae1bf254 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -651,7 +651,7 @@ LINE 1: SELECT public.dep_req2() || ' req3b' ^ HINT: No function matches the given name and argument types. You might need to add explicit type casts. QUERY: SELECT public.dep_req2() || ' req3b' -CONTEXT: SQL function "dep_req3b" during startup +CONTEXT: SQL function "dep_req3b" statement 1 DROP EXTENSION test_ext_req_schema3; ALTER EXTENSION test_ext_req_schema1 SET SCHEMA test_s_dep2; -- now ok SELECT test_s_dep2.dep_req1(); diff --git a/src/test/regress/expected/create_function_sql.out b/src/test/regress/expected/create_function_sql.out index 50aca5940ff..2ee7631044e 100644 --- a/src/test/regress/expected/create_function_sql.out +++ b/src/test/regress/expected/create_function_sql.out @@ -563,6 +563,20 @@ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1'; ERROR: cannot change routine kind DETAIL: "functest1" is a function. DROP FUNCTION functest1(a int); +-- early shutdown of set-returning functions +CREATE FUNCTION functest_srf0() RETURNS SETOF int +LANGUAGE SQL +AS $$ SELECT i FROM generate_series(1, 100) i $$; +SELECT functest_srf0() LIMIT 5; + functest_srf0 +--------------- + 1 + 2 + 3 + 4 + 5 +(5 rows) + -- inlining of set-returning functions CREATE TABLE functest3 (a int); INSERT INTO functest3 VALUES (1), (2), (3); @@ -666,6 +680,43 @@ SELECT * FROM voidtest5(3); ----------- (0 rows) +-- DDL within a SQL function can now affect later statements in the function; +-- though that doesn't work if check_function_bodies is on. +SET check_function_bodies TO off; +CREATE FUNCTION create_and_insert() RETURNS VOID LANGUAGE sql AS $$ + create table ddl_test (f1 int); + insert into ddl_test values (1.2); +$$; +SELECT create_and_insert(); + create_and_insert +------------------- + +(1 row) + +TABLE ddl_test; + f1 +---- + 1 +(1 row) + +CREATE FUNCTION alter_and_insert() RETURNS VOID LANGUAGE sql AS $$ + alter table ddl_test alter column f1 type numeric; + insert into ddl_test values (1.2); +$$; +SELECT alter_and_insert(); + alter_and_insert +------------------ + +(1 row) + +TABLE ddl_test; + f1 +----- + 1 + 1.2 +(2 rows) + +RESET check_function_bodies; -- Regression tests for bugs: -- Check that arguments that are R/W expanded datums aren't corrupted by -- multiple uses. This test knows that array_append() returns a R/W datum @@ -708,7 +759,7 @@ CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL ERROR: only one AS item needed for language "sql" -- Cleanup DROP SCHEMA temp_func_test CASCADE; -NOTICE: drop cascades to 30 other objects +NOTICE: drop cascades to 34 other objects DETAIL: drop cascades to function functest_a_1(text,date) drop cascades to function functest_a_2(text[]) drop cascades to function functest_a_3() @@ -732,12 +783,16 @@ drop cascades to function functest_s_10(text,date) drop cascades to function functest_s_13() drop cascades to function functest_s_15(integer) drop cascades to function functest_b_2(bigint) +drop cascades to function functest_srf0() drop cascades to function functest_sri1() drop cascades to function voidtest1(integer) drop cascades to function voidtest2(integer,integer) drop cascades to function voidtest3(integer) drop cascades to function voidtest4(integer) drop cascades to function voidtest5(integer) +drop cascades to function create_and_insert() +drop cascades to table ddl_test +drop cascades to function alter_and_insert() drop cascades to function double_append(anyarray,anyelement) DROP USER regress_unpriv_user; RESET search_path; diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out index 397a8b35d6d..c21be83aa4a 100644 --- a/src/test/regress/expected/rangefuncs.out +++ b/src/test/regress/expected/rangefuncs.out @@ -1885,7 +1885,7 @@ select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text); select * from array_to_set(array['one', 'two']) as t(f1 point,f2 text); ERROR: return type mismatch in function declared to return record DETAIL: Final statement returns integer instead of point at column 1. -CONTEXT: SQL function "array_to_set" during startup +CONTEXT: SQL function "array_to_set" statement 1 -- with "strict", this function can't be inlined in FROM explain (verbose, costs off) select * from array_to_set(array['one', 'two']) as t(f1 numeric(4,2),f2 text); diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 87929191d06..1c4e37d2249 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -4695,6 +4695,57 @@ RESET ROLE; DROP FUNCTION rls_f(); DROP VIEW rls_v; DROP TABLE rls_t; +-- Check that RLS changes invalidate SQL function plans +create table rls_t (c text); +create table test_t (c text); +insert into rls_t values ('a'), ('b'), ('c'), ('d'); +insert into test_t values ('a'), ('b'); +alter table rls_t enable row level security; +grant select on rls_t to regress_rls_alice; +grant select on test_t to regress_rls_alice; +create policy p1 on rls_t for select to regress_rls_alice + using (c = current_setting('rls_test.blah')); +-- Function changes row_security setting and so invalidates plan +create function rls_f(text) returns text +begin atomic + select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t; +end; +set plan_cache_mode to force_custom_plan; +-- Table owner bypasses RLS +select rls_f(c) from test_t order by rls_f; + rls_f +------------- + aoffa,b,c,d + boffa,b,c,d +(2 rows) + +-- For other users, changes in row_security setting +-- should lead to RLS error during query rewrite +set role regress_rls_alice; +select rls_f(c) from test_t order by rls_f; +ERROR: query would be affected by row-level security policy for table "rls_t" +CONTEXT: SQL function "rls_f" statement 1 +reset role; +set plan_cache_mode to force_generic_plan; +-- Table owner bypasses RLS, although cached plan will be invalidated +select rls_f(c) from test_t order by rls_f; + rls_f +------------- + aoffa,b,c,d + boffa,b,c,d +(2 rows) + +-- For other users, changes in row_security setting +-- should lead to plan invalidation and RLS error during query rewrite +set role regress_rls_alice; +select rls_f(c) from test_t order by rls_f; +ERROR: query would be affected by row-level security policy for table "rls_t" +CONTEXT: SQL function "rls_f" statement 1 +reset role; +reset plan_cache_mode; +reset rls_test.blah; +drop function rls_f(text); +drop table rls_t, test_t; -- -- Clean up objects -- diff --git a/src/test/regress/sql/create_function_sql.sql b/src/test/regress/sql/create_function_sql.sql index 89e9af3a499..68776be4c8d 100644 --- a/src/test/regress/sql/create_function_sql.sql +++ b/src/test/regress/sql/create_function_sql.sql @@ -328,6 +328,15 @@ CREATE OR REPLACE PROCEDURE functest1(a int) LANGUAGE SQL AS 'SELECT $1'; DROP FUNCTION functest1(a int); +-- early shutdown of set-returning functions + +CREATE FUNCTION functest_srf0() RETURNS SETOF int +LANGUAGE SQL +AS $$ SELECT i FROM generate_series(1, 100) i $$; + +SELECT functest_srf0() LIMIT 5; + + -- inlining of set-returning functions CREATE TABLE functest3 (a int); @@ -385,6 +394,31 @@ CREATE FUNCTION voidtest5(a int) RETURNS SETOF VOID LANGUAGE SQL AS $$ SELECT generate_series(1, a) $$ STABLE; SELECT * FROM voidtest5(3); +-- DDL within a SQL function can now affect later statements in the function; +-- though that doesn't work if check_function_bodies is on. + +SET check_function_bodies TO off; + +CREATE FUNCTION create_and_insert() RETURNS VOID LANGUAGE sql AS $$ + create table ddl_test (f1 int); + insert into ddl_test values (1.2); +$$; + +SELECT create_and_insert(); + +TABLE ddl_test; + +CREATE FUNCTION alter_and_insert() RETURNS VOID LANGUAGE sql AS $$ + alter table ddl_test alter column f1 type numeric; + insert into ddl_test values (1.2); +$$; + +SELECT alter_and_insert(); + +TABLE ddl_test; + +RESET check_function_bodies; + -- Regression tests for bugs: -- Check that arguments that are R/W expanded datums aren't corrupted by diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index f61dbbf9581..9da967a9ef2 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -2307,6 +2307,50 @@ DROP FUNCTION rls_f(); DROP VIEW rls_v; DROP TABLE rls_t; +-- Check that RLS changes invalidate SQL function plans +create table rls_t (c text); +create table test_t (c text); +insert into rls_t values ('a'), ('b'), ('c'), ('d'); +insert into test_t values ('a'), ('b'); +alter table rls_t enable row level security; +grant select on rls_t to regress_rls_alice; +grant select on test_t to regress_rls_alice; +create policy p1 on rls_t for select to regress_rls_alice + using (c = current_setting('rls_test.blah')); + +-- Function changes row_security setting and so invalidates plan +create function rls_f(text) returns text +begin atomic + select set_config('rls_test.blah', $1, true) || set_config('row_security', 'false', true) || string_agg(c, ',' order by c) from rls_t; +end; + +set plan_cache_mode to force_custom_plan; + +-- Table owner bypasses RLS +select rls_f(c) from test_t order by rls_f; + +-- For other users, changes in row_security setting +-- should lead to RLS error during query rewrite +set role regress_rls_alice; +select rls_f(c) from test_t order by rls_f; +reset role; + +set plan_cache_mode to force_generic_plan; + +-- Table owner bypasses RLS, although cached plan will be invalidated +select rls_f(c) from test_t order by rls_f; + +-- For other users, changes in row_security setting +-- should lead to plan invalidation and RLS error during query rewrite +set role regress_rls_alice; +select rls_f(c) from test_t order by rls_f; +reset role; + +reset plan_cache_mode; +reset rls_test.blah; +drop function rls_f(text); +drop table rls_t, test_t; + -- -- Clean up objects -- |