1
0
mirror of https://github.com/postgres/postgres.git synced 2025-12-19 17:02:53 +03:00

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
This commit is contained in:
Tom Lane
2025-04-02 14:05:50 -04:00
parent e9e7b66044
commit 0dca5d68d7
24 changed files with 2186 additions and 1043 deletions

View File

@@ -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();

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
--

View File

@@ -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

View File

@@ -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
--