public inbox for [email protected]help / color / mirror / Atom feed
Re: proposal: schema variables 24+ messages / 4 participants [nested] [flat]
* Re: proposal: schema variables @ 2024-11-16 14:27 Dmitry Dolgov <[email protected]> 0 siblings, 2 replies; 24+ messages in thread From: Dmitry Dolgov @ 2024-11-16 14:27 UTC (permalink / raw) To: Pavel Stehule <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> > On Sat, Nov 16, 2024 at 07:10:31AM GMT, Pavel Stehule wrote: Sorry, got distracted. Let me try to answer step by step. > > As far as I recall, last time this topic was discussed in hackers, two > > options were proposed: the one with VARIABLE(name), what you mention > > here; and another one with adding variables to the FROM clause. The > > VARIABLE(...) syntax didn't get much negative feedback, so I guess why > > not -- if you find it fitting, it would be interesting to see the > > implementation. > > > > I'm afraid it should not be just an alternative syntax, but the only one > > allowed, because otherwise I don't see how scenarious like "drop a > > column with the same name" could be avoided. As in the previous thread: > > > > -- we've got a variable b at the same time > > SELECT a, b FROM table1; > > > > I am sorry, but I am in very strong opposition against this idea. Nobody > did reply to my questions, that can change my opinion. From your reply it's not quite clear, are you opposed to have a mandatory VARIABLE syntax, or having variables in the FROM clause? My main proposal was about the former, but the points that are following seems to talk about the latter. I think it's fine to reject the idea about the FROM clause, as long as you got some reasonable arguments. > > Then dropping the column b, but everything still works beause the > > variable b got silently picked up. But if it would be required to say > > VARIABLE(b), then all fine. > > but same risk you have any time in plpgsql - all time. I don't remember any > bug report related to this issue. Which exactly scenario about plpgsql do you have in mind? Just have tried to declare a variable inside a plpgsql function with the same name as a table column, and got an error about an ambiguous reference. > Theoretically, variables can have the same names as tables. The table > overshadows the variable, so it can work. But when somebody drops the > variable, then the query still can work. So requirement of usage variable > in FROM clause protects us just against drop column, but not against > dropping table. In Postgres the dropping table is possibly risky due > search_path (that introduces shadowing concept) without introduction > variables. There is a possibility of this issue, but how common is this > issue? This sounds to me like an argument against allowing name clashing between variables and tables. It makes even more sense, since session variables are in many ways similar to tables. > I think this issue can be partially similar to creating two equally named > tables in different schemas (both schemas are in search path). When you > drop one table, the query will work, but the result is different. It is the > same issue. The SQL has no concept of shadowing and on the base line it is > not necessary. The point is that most of users are aware about schemas and search path dangers. But to me such a precedent is not an excuse to introduce a new feature with similar traps, which folks would have to learn by making mistakes. Judging from the feedback to this patch over time, I've got an impression that lots of people are also not fans of that. > > Then dropping the column b, but everything still works beause the > > variable b got silently picked up. But if it would be required to say > > VARIABLE(b), then all fine. > > > > In this scenario you will get a warning related to variable shadowing > (before you drop a column). > > [...] > > What do you think about the following design? I can implement a warning > "variable_usage_guard" when the variable is accessed without using > VARIABLE() syntax. We can discuss later if this warning can be enabled by > default or not. There I am open to any variant. I don't follow what are you winning by that? In the context of problem above (i.e. dropping a column), such a warning is functionally equivalend to a warning about shadowing. The problem is that it doesn't sound very appealing to have a feature, which requires some extra efforts to be used in a right way (e.g. put everything into a special vars schema, or keep an eye on logs). Most certainly there are such bits in PostgreSQL today, with all the best practices, crowd wisdom, etc. But the bar for new features in this sense is much higher, you can see it from the feedback to this patch. Thus I believe it makes sense, from purely tactical reasons, to not try to convince half of the community to lower the bar, but instead try to modify the feature to make it more acceptable, even if some parts you might not like. Btw, could you repeat, what was exactly your argument against mandatory VARIABLE() syntax? It's somehow scattered across many replies, would be great to summarize it in a couple of phrases. > Shadowing by self is not an issue, probably, but it is a signal of code > quality problems. Agree, but I'm afraid code quality of an average application using PostgreSQL is quite low, so here we are. As a side note, I've recently caught myself thinking "it would be cool to have session variables here". The use case was preparing a policy for RLS, based on some session-level data set by an application. This session-level data is of a composite data type, so simple current_setting is cumbersome to use, and a temporary table will be dropped at the end, taking the policy with it due to the recorded dependency between them. Thus a session variable of some composite type sounds like a good fit. ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2024-11-16 22:49 Pavel Stehule <[email protected]> parent: Dmitry Dolgov <[email protected]> 1 sibling, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2024-11-16 22:49 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> so 16. 11. 2024 v 15:27 odesílatel Dmitry Dolgov <[email protected]> napsal: > > On Sat, Nov 16, 2024 at 07:10:31AM GMT, Pavel Stehule wrote: > > Sorry, got distracted. Let me try to answer step by step. > > > > As far as I recall, last time this topic was discussed in hackers, two > > > options were proposed: the one with VARIABLE(name), what you mention > > > here; and another one with adding variables to the FROM clause. The > > > VARIABLE(...) syntax didn't get much negative feedback, so I guess why > > > not -- if you find it fitting, it would be interesting to see the > > > implementation. > > > > > > I'm afraid it should not be just an alternative syntax, but the only > one > > > allowed, because otherwise I don't see how scenarious like "drop a > > > column with the same name" could be avoided. As in the previous thread: > > > > > > -- we've got a variable b at the same time > > > SELECT a, b FROM table1; > > > > > > > I am sorry, but I am in very strong opposition against this idea. Nobody > > did reply to my questions, that can change my opinion. > > From your reply it's not quite clear, are you opposed to have a mandatory > VARIABLE syntax, or having variables in the FROM clause? My main proposal > was > about the former, but the points that are following seems to talk about the > latter. I think it's fine to reject the idea about the FROM clause, as > long as > you got some reasonable arguments. > I am against a requirement to specify a variable in the FROM clause. > > > > Then dropping the column b, but everything still works beause the > > > variable b got silently picked up. But if it would be required to say > > > VARIABLE(b), then all fine. > > > > but same risk you have any time in plpgsql - all time. I don't remember > any > > bug report related to this issue. > > Which exactly scenario about plpgsql do you have in mind? Just have tried > to > declare a variable inside a plpgsql function with the same name as a table > column, and got an error about an ambiguous reference. > Until you execute the query, you cannot know if there is a conflict or not. So you can change table structure and you can change the procedure's code, and there can be an invisible conflict until execution and query evaluation. The conflict between PL/pgSQL and SQL raises an error. The conflict between session variables and SQL raises warnings. The issue is detected. > > > Theoretically, variables can have the same names as tables. The table > > overshadows the variable, so it can work. But when somebody drops the > > variable, then the query still can work. So requirement of usage variable > > in FROM clause protects us just against drop column, but not against > > dropping table. In Postgres the dropping table is possibly risky due > > search_path (that introduces shadowing concept) without introduction > > variables. There is a possibility of this issue, but how common is this > > issue? > > This sounds to me like an argument against allowing name clashing between > variables and tables. It makes even more sense, since session variables > are in > many ways similar to tables. > > It doesn't help too much. It can fix just one issue. But you can have tables with the same name in different schemas inside schemas from search_path. Unique table names solve nothing. > > I think this issue can be partially similar to creating two equally named > > tables in different schemas (both schemas are in search path). When you > > drop one table, the query will work, but the result is different. It is > the > > same issue. The SQL has no concept of shadowing and on the base line it > is > > not necessary. > > The point is that most of users are aware about schemas and search path > dangers. But to me such a precedent is not an excuse to introduce a new > feature > with similar traps, which folks would have to learn by making mistakes. > Judging > from the feedback to this patch over time, I've got an impression that > lots of > people are also not fans of that. > Unfortunately - I don't believe so there is some syntax without traps. You can check all implementations in other databases. These designs are very different, and all have some issues and all have some limits. It is native - you are trying to join the procedural and functional world. I understand the risks. These risks are there. But there is no silver bullet - all proposed designs fixed just one case, and not others, and then I don't see a strong enough benefit to introduce design that is far from common usage. Maybe I have a different experience, because I am a man from the stored procedures area, and the risk of collisions is a known issue well solved by common conventions and in postgres by plpgsql.variable_conflict setting. The proposed patch set has very similar functionality. I think the introduction of VARIABLE(xx) syntax and safe syntax guard warning the usage of variables can be safe in how it is possible. But still I want to allow "short" "usual" usage to people who use a safe convention. There is no risk when you use a safe prefix or safe schema. > > > > Then dropping the column b, but everything still works beause the > > > variable b got silently picked up. But if it would be required to say > > > VARIABLE(b), then all fine. > > > > > > > In this scenario you will get a warning related to variable shadowing > > (before you drop a column). > > > > [...] > > > > What do you think about the following design? I can implement a warning > > "variable_usage_guard" when the variable is accessed without using > > VARIABLE() syntax. We can discuss later if this warning can be enabled by > > default or not. There I am open to any variant. > > I don't follow what are you winning by that? In the context of problem > above > (i.e. dropping a column), such a warning is functionally equivalend to a > warning about shadowing. > > The problem is that it doesn't sound very appealing to have a feature, > which > requires some extra efforts to be used in a right way (e.g. put everything > into > a special vars schema, or keep an eye on logs). Most certainly there are > such > bits in PostgreSQL today, with all the best practices, crowd wisdom, etc. > But > the bar for new features in this sense is much higher, you can see it from > the > feedback to this patch. Thus I believe it makes sense, from purely tactical > reasons, to not try to convince half of the community to lower the bar, but > instead try to modify the feature to make it more acceptable, even if some > parts you might not like. > > Btw, could you repeat, what was exactly your argument against mandatory > VARIABLE() syntax? It's somehow scattered across many replies, would be > great > to summarize it in a couple of phrases. > > > Shadowing by self is not an issue, probably, but it is a signal of code > > quality problems. > > Agree, but I'm afraid code quality of an average application using > PostgreSQL > is quite low, so here we are. > > As a side note, I've recently caught myself thinking "it would be cool to > have > session variables here". The use case was preparing a policy for RLS, > based on > some session-level data set by an application. This session-level data is > of a > composite data type, so simple current_setting is cumbersome to use, and a > temporary table will be dropped at the end, taking the policy with it due > to > the recorded dependency between them. Thus a session variable of some > composite > type sounds like a good fit. > ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2024-11-17 04:41 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Pavel Stehule @ 2024-11-17 04:41 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> so 16. 11. 2024 v 23:49 odesílatel Pavel Stehule <[email protected]> napsal: > > > so 16. 11. 2024 v 15:27 odesílatel Dmitry Dolgov <[email protected]> > napsal: > >> > On Sat, Nov 16, 2024 at 07:10:31AM GMT, Pavel Stehule wrote: >> >> Sorry, got distracted. Let me try to answer step by step. >> >> > > As far as I recall, last time this topic was discussed in hackers, two >> > > options were proposed: the one with VARIABLE(name), what you mention >> > > here; and another one with adding variables to the FROM clause. The >> > > VARIABLE(...) syntax didn't get much negative feedback, so I guess why >> > > not -- if you find it fitting, it would be interesting to see the >> > > implementation. >> > > >> > > I'm afraid it should not be just an alternative syntax, but the only >> one >> > > allowed, because otherwise I don't see how scenarious like "drop a >> > > column with the same name" could be avoided. As in the previous >> thread: >> > > >> > > -- we've got a variable b at the same time >> > > SELECT a, b FROM table1; >> > > >> > >> > I am sorry, but I am in very strong opposition against this idea. >> Nobody >> > did reply to my questions, that can change my opinion. >> >> From your reply it's not quite clear, are you opposed to have a mandatory >> VARIABLE syntax, or having variables in the FROM clause? My main proposal >> was >> about the former, but the points that are following seems to talk about >> the >> latter. I think it's fine to reject the idea about the FROM clause, as >> long as >> you got some reasonable arguments. >> > > I am against a requirement to specify a variable in the FROM clause. > > >> >> > > Then dropping the column b, but everything still works beause the >> > > variable b got silently picked up. But if it would be required to say >> > > VARIABLE(b), then all fine. >> > >> > but same risk you have any time in plpgsql - all time. I don't remember >> any >> > bug report related to this issue. >> >> Which exactly scenario about plpgsql do you have in mind? Just have tried >> to >> declare a variable inside a plpgsql function with the same name as a table >> column, and got an error about an ambiguous reference. >> > > Until you execute the query, you cannot know if there is a conflict or > not. So you can change table structure and you can change the procedure's > code, and there can be an invisible conflict until execution and query > evaluation. The conflict between PL/pgSQL and SQL raises an error. The > conflict between session variables and SQL raises warnings. The issue is > detected. > > > >> >> > Theoretically, variables can have the same names as tables. The table >> > overshadows the variable, so it can work. But when somebody drops the >> > variable, then the query still can work. So requirement of usage >> variable >> > in FROM clause protects us just against drop column, but not against >> > dropping table. In Postgres the dropping table is possibly risky due >> > search_path (that introduces shadowing concept) without introduction >> > variables. There is a possibility of this issue, but how common is this >> > issue? >> >> This sounds to me like an argument against allowing name clashing between >> variables and tables. It makes even more sense, since session variables >> are in >> many ways similar to tables. >> >> > It doesn't help too much. It can fix just one issue. But you can have > tables with the same name in different schemas inside schemas from > search_path. Unique table names solve nothing. > the combination of pg_class and pg_attribute cannot describe scalar variables (without hacks). Then you need to enhance pg_class, which can be confusing. And on the second hand almost all columns in pg_class have no sense for variables. And when variables and tables are in different tables, you cannot ensure a unique name. Variables are similar to tables only in possibility to hold a value. That is all. But variables don't store data to file, don't store data in pages, don't allow usage of other storages or formats, and don't support foreign storage. The similarity between variables and tables is like the similarity between horses and cars. Both can help with moving. > >> > I think this issue can be partially similar to creating two equally >> named >> > tables in different schemas (both schemas are in search path). When you >> > drop one table, the query will work, but the result is different. It is >> the >> > same issue. The SQL has no concept of shadowing and on the base line it >> is >> > not necessary. >> >> The point is that most of users are aware about schemas and search path >> dangers. But to me such a precedent is not an excuse to introduce a new >> feature >> with similar traps, which folks would have to learn by making mistakes. >> Judging >> from the feedback to this patch over time, I've got an impression that >> lots of >> people are also not fans of that. >> > > Unfortunately - I don't believe so there is some syntax without traps. You > can check all implementations in other databases. These designs are very > different, and all have some issues and all have some limits. It is native > - you are trying to join the procedural and functional world. > > I understand the risks. These risks are there. But there is no silver > bullet - all proposed designs fixed just one case, and not others, and then > I don't see a strong enough benefit to introduce design that is far from > common usage. Maybe I have a different experience, because I am a man from > the stored procedures area, and the risk of collisions is a known issue > well solved by common conventions and in postgres by > plpgsql.variable_conflict setting. The proposed patch set has very similar > functionality. I think the introduction of VARIABLE(xx) syntax and safe > syntax guard warning the usage of variables can be safe in how it is > possible. But still I want to allow "short" "usual" usage to people who use > a safe convention. There is no risk when you use a safe prefix or safe > schema. > > > >> >> > > Then dropping the column b, but everything still works beause the >> > > variable b got silently picked up. But if it would be required to say >> > > VARIABLE(b), then all fine. >> > > >> > >> > In this scenario you will get a warning related to variable shadowing >> > (before you drop a column). >> > >> > [...] >> > >> > What do you think about the following design? I can implement a warning >> > "variable_usage_guard" when the variable is accessed without using >> > VARIABLE() syntax. We can discuss later if this warning can be enabled >> by >> > default or not. There I am open to any variant. >> >> I don't follow what are you winning by that? In the context of problem >> above >> (i.e. dropping a column), such a warning is functionally equivalend to a >> warning about shadowing. >> >> The problem is that it doesn't sound very appealing to have a feature, >> which >> requires some extra efforts to be used in a right way (e.g. put >> everything into >> a special vars schema, or keep an eye on logs). Most certainly there are >> such >> bits in PostgreSQL today, with all the best practices, crowd wisdom, etc. >> But >> the bar for new features in this sense is much higher, you can see it >> from the >> feedback to this patch. Thus I believe it makes sense, from purely >> tactical >> reasons, to not try to convince half of the community to lower the bar, >> but >> instead try to modify the feature to make it more acceptable, even if some >> parts you might not like. >> >> Btw, could you repeat, what was exactly your argument against mandatory >> VARIABLE() syntax? It's somehow scattered across many replies, would be >> great >> to summarize it in a couple of phrases. >> >> > Shadowing by self is not an issue, probably, but it is a signal of code >> > quality problems. >> >> Agree, but I'm afraid code quality of an average application using >> PostgreSQL >> is quite low, so here we are. >> >> As a side note, I've recently caught myself thinking "it would be cool to >> have >> session variables here". The use case was preparing a policy for RLS, >> based on >> some session-level data set by an application. This session-level data is >> of a >> composite data type, so simple current_setting is cumbersome to use, and a >> temporary table will be dropped at the end, taking the policy with it due >> to >> the recorded dependency between them. Thus a session variable of some >> composite >> type sounds like a good fit. >> > ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2024-11-19 19:14 Pavel Stehule <[email protected]> parent: Dmitry Dolgov <[email protected]> 1 sibling, 2 replies; 24+ messages in thread From: Pavel Stehule @ 2024-11-19 19:14 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> Hi I wrote POC of VARIABLE(varname) syntax support here is a copy from regress test SET session_variables_ambiguity_warning TO on; SET session_variables_use_fence_warning_guard TO on; SET search_path TO 'public'; CREATE VARIABLE a AS int; LET a = 10; CREATE TABLE test_table(a int, b int); INSERT INTO test_table VALUES(20, 20); -- no warning SELECT a; a.. ---- 10 (1 row) -- warning variable is shadowed SELECT a, b FROM test_table; WARNING: session variable "a" is shadowed LINE 1: SELECT a, b FROM test_table; ^ DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. a | b.. ----+---- 20 | 20 (1 row) -- no warning SELECT variable(a) FROM test_table; a.. ---- 10 (1 row) ALTER TABLE test_table DROP COLUMN a; -- warning - variable fence is not used SELECT a, b FROM test_table; WARNING: session variable "a" is not used inside variable fence LINE 1: SELECT a, b FROM test_table; ^ DETAIL: The collision of session variable' names and column names is possible. a | b.. ----+---- 10 | 20 (1 row) -- no warning SELECT variable(a), b FROM test_table; a | b.. ----+---- 10 | 20 (1 row) DROP VARIABLE a; DROP TABLE test_table; SET session_variables_ambiguity_warning TO DEFAULT; SET session_variables_use_fence_warning_guard TO DEFAULT; SET search_path TO DEFAULT; Regards Pavel Attachments: [text/x-patch] 0020-pg_restore-A-variable.patch (2.8K, 3-0020-pg_restore-A-variable.patch) download | inline diff: From 7271601152ba10bde00fd54a982861cd63003273 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 21 Jul 2024 17:04:41 +0200 Subject: [PATCH 20/21] pg_restore -A, --variable possibility to restore session variable specified by name --- doc/src/sgml/ref/pg_restore.sgml | 11 +++++++++++ src/bin/pg_dump/pg_restore.c | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index b8b27e1719e..7a3a4eabe25 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -106,6 +106,17 @@ PostgreSQL documentation </listitem> </varlistentry> + <varlistentry> + <term><option>-A <replaceable class="parameter">session_variable</replaceable></option></term> + <term><option>--variable=<replaceable class="parameter">session_variable</replaceable></option></term> + <listitem> + <para> + Restore a named session variable only. Multiple session variables may + be specified with multiple <option>-A</option> switches. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-c</option></term> <term><option>--clean</option></term> diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index f2c1020d053..f916736d761 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -104,6 +104,7 @@ main(int argc, char **argv) {"trigger", 1, NULL, 'T'}, {"use-list", 1, NULL, 'L'}, {"username", 1, NULL, 'U'}, + {"variable", 1, NULL, 'A'}, {"verbose", 0, NULL, 'v'}, {"single-transaction", 0, NULL, '1'}, @@ -154,7 +155,7 @@ main(int argc, char **argv) } } - while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", + while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", cmdopts, NULL)) != -1) { switch (c) @@ -162,6 +163,11 @@ main(int argc, char **argv) case 'a': /* Dump data only */ opts->dataOnly = 1; break; + case 'A': /* vAriable */ + opts->selTypes = 1; + opts->selVariable = 1; + simple_string_list_append(&opts->variableNames, optarg); + break; case 'c': /* clean (i.e., drop) schema prior to create */ opts->dropSchema = 1; break; @@ -462,6 +468,7 @@ usage(const char *progname) printf(_("\nOptions controlling the restore:\n")); printf(_(" -a, --data-only restore only the data, no schema\n")); + printf(_(" -A, --variable=NAME restore named session variable\n")); printf(_(" -c, --clean clean (drop) database objects before recreating\n")); printf(_(" -C, --create create the target database\n")); printf(_(" -e, --exit-on-error exit on error, default is to continue\n")); -- 2.47.0 [text/x-patch] 0019-transactional-variables.patch (39.2K, 4-0019-transactional-variables.patch) download | inline diff: From 4cf664b8b049d206479f4dd9650b123851b1dd9c Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:11:33 +0100 Subject: [PATCH 19/21] transactional variables This commit implements transactional variables. The content of transactional session variables is sensitive to transactions and subtransactions. Any transactional variable holds a history of values necessary for revert. This history is cleaned (purged) when a) we read the variable, b) when the transaction is finished. We don't try to purge, when the last modification of a variable is from current subtransaction, or when purge was processed in current subtransaction. This patch is based on my work from Feb 2020 and it is related to discussion about features related to session variables. Now, when the all features are separated to isolated patches I can revitalize this patch, because it doesn't increase complexity of basic patches. Unlike other patches, this patch is without any review at this moment (Feb 2024), but the feature should be fully functional for people who are interested about this feature (for testing). --- doc/src/sgml/catalogs.sgml | 12 + doc/src/sgml/ref/create_variable.sgml | 10 +- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 301 +++++++++++++++++- src/backend/parser/gram.y | 50 +-- src/bin/pg_dump/pg_dump.c | 10 +- src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 +++ src/bin/psql/describe.c | 4 +- src/bin/psql/tab-complete.in.c | 5 + src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/kwlist.h | 1 + src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 110 ++++++- src/test/regress/sql/session_variables.sql | 58 ++++ 16 files changed, 592 insertions(+), 50 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 9a8886fc69a..1064b3749e3 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9859,6 +9859,18 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry><structfield>varistransact</structfield></entry> + <entry><type>boolean</type></entry> + <entry></entry> + <entry> + True, when the variable is <quote>transactional</quote>. In the case + of a transaction rollback, transactional variables are reset to the + value they had when the transaction started. The default value is + <literal>false</literal>. + </entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>vareoxaction</structfield> <type>char</type> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 00938743c0d..b73f032ddbb 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] [ { TRANSACTIONAL | TRANSACTION } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> @@ -47,6 +47,14 @@ CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replac same as regular variables in procedural languages. </para> + <para> + When a schema variable is created with a + <command>CREATE TRANSACTIONAL VARIABLE</command> command, the variables + content changes are transactional: in case of rollback, they are reset to + their value at the beginning of the transaction or the latest subtransaction. + The variable content is only hold in memory, and thus is not persistent. + </para> + <para> Session variables are retrieved by the <command>SELECT</command> command. Their value is set with the <command>LET</command> command. diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index f261e3fd770..771c2816048 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -42,6 +42,7 @@ static ObjectAddress create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -59,6 +60,7 @@ create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -123,6 +125,7 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); + values[Anum_pg_variable_varistransact - 1] = BoolGetDatum(is_transact); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -267,6 +270,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) stmt->if_not_exists, stmt->not_null, stmt->is_immutable, + stmt->is_transact, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 7f34f93b542..978c84c9bdb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -47,6 +47,19 @@ typedef struct SVariableXActDropItem SubTransactionId deleting_subid; } SVariableXActDropItem; +/* + * Used for transactional variables. Holds prev version. + */ +typedef struct PrevValue +{ + Datum value; + bool isnull; + + SubTransactionId modify_subid; + + struct PrevValue *prev_value; +} PrevValue; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -68,6 +81,25 @@ typedef struct SVariableData bool isnull; Datum value; + /* + * We don't need stack versions modified in same subtransaction. + * Used by transactional variables only. The value of transactional + * variable can be returned immediately when modify_subid is same + * like current subid. + */ + SubTransactionId modify_subid; + + /* + * When the modify_subid is different than current subid, then + * we need to recheck versions and throw versions related to + * reverted transactions. When purge_subid is same like current subid + * we can return the value of transaction variable without this + * recheck. + */ + SubTransactionId purge_subid; + + PrevValue *prev_value; + Oid typid; int16 typlen; bool typbyval; @@ -86,6 +118,7 @@ typedef struct SVariableData bool not_null; bool is_immutable; + bool is_transact; bool reset_at_eox; @@ -125,6 +158,9 @@ static bool needs_validation = false; */ static bool has_session_variables_with_reset_at_eox = false; +/* true, when transactional variables was modified */ +static bool has_modified_transactional_variables = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -140,6 +176,7 @@ static List *xact_drop_items = NIL; static void register_session_variable_xact_drop(Oid varid); static void unregister_session_variable_xact_drop(Oid varid); +static bool purge_session_variable(SVariable svar); /* * Callback function for session variable invalidation. @@ -292,8 +329,25 @@ unregister_session_variable_xact_drop(Oid varid) * Release stored value, free memory */ static void -free_session_variable_value(SVariable svar) +free_session_variable_value(SVariable svar, bool deep_free) { + if (deep_free) + { + PrevValue *prev_value = svar->prev_value; + PrevValue *next_value; + + while (prev_value) + { + if (!svar->typbyval) + pfree(DatumGetPointer(prev_value->value)); + + next_value = prev_value->prev_value; + pfree(prev_value); + + prev_value = next_value; + } + } + /* clean the current value */ if (!svar->isnull) { @@ -390,7 +444,7 @@ remove_invalid_session_variables(bool atEOX) { Oid varid = svar->varid; - free_session_variable_value(svar); + free_session_variable_value(svar, true); hash_search(sessionvars, &varid, HASH_REMOVE, NULL); svar = NULL; } @@ -426,6 +480,94 @@ remove_session_variables_with_reset_at_eox(void) has_session_variables_with_reset_at_eox = false; } +/* + * remove prev values at eox + */ +static void +remove_prev_values_at_eox(bool isCommit) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_modified_transactional_variables) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->is_transact && svar->modify_subid != InvalidSubTransactionId) + { + if (isCommit) + { + if (purge_session_variable(svar)) + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + else + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId) + break; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + if (prev_value) + { + svar->value = prev_value->value; + svar->isnull = prev_value->isnull; + + pfree(prev_value); + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + + /* when svar is still valid (not removed from sessionvars */ + if (svar) + { + svar->modify_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + } + } + } + + has_modified_transactional_variables = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -473,6 +615,8 @@ AtPreEOXact_SessionVariables(bool isCommit) remove_invalid_session_variables(true); } + remove_prev_values_at_eox(isCommit); + /* * We have to clean xact_drop_items. All related variables are dropped * now, or lost inside aborted transaction. @@ -618,6 +762,7 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->not_null = varform->varnotnull; svar->is_immutable = varform->varisimmutable; + svar->is_transact = varform->varistransact; svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; @@ -643,6 +788,17 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->isnull = true; svar->value = (Datum) 0; + if (svar->is_transact) + { + svar->modify_subid = GetCurrentSubTransactionId(); + has_modified_transactional_variables = true; + } + else + svar->modify_subid = InvalidSubTransactionId; + + svar->purge_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + svar->is_valid = true; svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, @@ -676,6 +832,69 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) ReleaseSysCache(tup); } +/* + * Try to remove all previous versions related to reverted transactions. + * Returns true, when valid version was found. + */ +static bool +purge_session_variable(SVariable svar) +{ + SubTransactionId current_subid; + PrevValue *prev_value; + bool found = true; + + Assert(svar->is_transact); + + if (svar->modify_subid == InvalidSubTransactionId) + return true; + + current_subid = GetCurrentSubTransactionId(); + + if (svar->modify_subid == current_subid) + return true; + + if (svar->purge_subid == current_subid) + return true; + + if (SubTransactionIsActive(svar->modify_subid)) + { + svar->purge_subid = current_subid; + return true; + } + + prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId || + SubTransactionIsActive(current_pv->modify_subid)) + { + svar->value = current_pv->value; + svar->isnull = current_pv->isnull; + svar->modify_subid = current_pv->modify_subid; + + prev_value = current_pv->prev_value; + pfree(current_pv); + + found = true; + break; + } + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + svar->prev_value = prev_value; + svar->purge_subid = current_subid; + + return found; +} + /* * Assign a new value to the session variable. It is copied to * SVariableMemoryContext if necessary. @@ -688,6 +907,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Datum newval; SVariableData locsvar, *_svar; + SubTransactionId current_subid = GetCurrentSubTransactionId(); + SubTransactionId prev_purge_subid = InvalidSubTransactionId; + bool save_prev_value; + PrevValue *prev_value; Assert(svar); Assert(!isnull || value == (Datum) 0); @@ -723,6 +946,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else _svar = svar; + if (_svar->is_transact && _svar->create_lsn == svar->create_lsn) + { + Assert(svar->typid == _svar->typid); + Assert(svar->typbyval == _svar->typbyval); + Assert(svar->typlen == _svar->typlen); + + save_prev_value = svar->modify_subid != current_subid; + prev_value = svar->prev_value; + } + else + { + save_prev_value = false; + prev_value = NULL; + } + if (!isnull) { MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); @@ -734,7 +972,37 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else newval = value; - free_session_variable_value(svar); + if (save_prev_value) + { + volatile PrevValue *new_prev_value; + + PG_TRY(); + { + new_prev_value = MemoryContextAlloc(SVariableMemoryContext, + sizeof(PrevValue)); + } + PG_CATCH(); + { + /* release mem from persistent content */ + if (newval != value) + pfree(DatumGetPointer(newval)); + PG_RE_THROW(); + } + PG_END_TRY(); + + new_prev_value->value = svar->value; + new_prev_value->isnull = svar->isnull; + new_prev_value->modify_subid = svar->modify_subid; + new_prev_value->prev_value = prev_value; + + prev_value = (PrevValue *) new_prev_value; + prev_purge_subid = svar->purge_subid; + + has_modified_transactional_variables = true; + + } + else + free_session_variable_value(svar, prev_value == NULL); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", get_namespace_name(get_session_variable_namespace(svar->varid)), @@ -747,6 +1015,9 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + svar->modify_subid = current_subid; + svar->purge_subid = prev_purge_subid; + svar->prev_value = prev_value; /* don't allow more changes of value when variable is IMMUTABLE */ if (svar->is_immutable) @@ -848,6 +1119,8 @@ get_session_variable(Oid varid) else svar->is_valid = false; +reinit: + /* * Force setup for not yet initialized variables or variables that cannot * be validated. @@ -880,6 +1153,28 @@ get_session_variable(Oid varid) varid); } + /* + * Transactional variables should be purged before (remove + * versions created by possibly reverted subtransactions). + */ + if (svar->is_transact && + svar->modify_subid != GetCurrentSubTransactionId() && + svar->modify_subid != InvalidSubTransactionId) + { + if (!purge_session_variable(svar)) + { + /* force reinit */ + svar->is_valid = false; + + /* + * In next iteration modify_subid should be + * InvalidSubTransactionId or current subid, + * so there is not risk of infinity cycle. + */ + goto reinit; + } + } + /* ensure the returned data is still of the correct domain */ if (svar->is_domain) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index bc8e25b1933..af9d640c0e2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,7 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt -%type <boolean> OptNotNull OptImmutable +%type <boolean> OptNotNull OptImmutable OptTransactional /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN - TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM + TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSACTIONAL TRANSFORM TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P @@ -5240,31 +5240,33 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptTransactional OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $5->relpersistence = $2; - n->is_immutable = $3; - n->variable = $5; - n->typeName = $7; - n->collClause = (CollateClause *) $8; - n->not_null = $9; - n->defexpr = $10; - n->XactEndAction = $11; + $6->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->not_null = $10; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptTransactional OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $8->relpersistence = $2; - n->is_immutable = $3; - n->variable = $8; - n->typeName = $10; - n->collClause = (CollateClause *) $11; - n->not_null = $12; - n->defexpr = $13; - n->XactEndAction = $14; + $9->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $9; + n->typeName = $11; + n->collClause = (CollateClause *) $12; + n->not_null = $13; + n->defexpr = $14; + n->XactEndAction = $15; n->if_not_exists = true; $$ = (Node *) n; } @@ -5292,6 +5294,12 @@ OptImmutable: IMMUTABLE { $$ = true; } | /* EMPTY */ { $$ = false; } ; +OptTransactional: + TRANSACTION { $$ = true; } + | TRANSACTIONAL { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -18084,6 +18092,7 @@ unreserved_keyword: | TEXT_P | TIES | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TRIGGER | TRUNCATE @@ -18732,6 +18741,7 @@ bare_label_keyword: | TIMESTAMP | TRAILING | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TREAT | TRIGGER diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 59f4103ca4f..762cd020ea9 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5439,6 +5439,7 @@ getVariables(Archive *fout) int i_varcollation; int i_varnotnull; int i_varisimmutable; + int i_varistransact; int i_varacl; int i_acldefault; int i, @@ -5463,6 +5464,7 @@ getVariables(Archive *fout) " END AS varcollation,\n" " v.varnotnull,\n" " v.varisimmutable,\n" + " v.varistransact,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5485,6 +5487,7 @@ getVariables(Archive *fout) i_varcollation = PQfnumber(res, "varcollation"); i_varnotnull = PQfnumber(res, "varnotnull"); i_varisimmutable = PQfnumber(res, "varisimmutable"); + i_varistransact = PQfnumber(res, "varistransact"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5513,6 +5516,7 @@ getVariables(Archive *fout) varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; + varinfo[i].varistransact = *(PQgetvalue(res, i, i_varistransact)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5560,6 +5564,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vardefexpr; const char *varxactendaction; const char *varisimmutable; + const char *varistransact; Oid varcollation; bool varnotnull; @@ -5577,12 +5582,13 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) varcollation = varinfo->varcollation; varnotnull = varinfo->varnotnull; varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; + varistransact = varinfo->varistransact ? "TRANSACTIONAL " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", - varisimmutable, qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %s%sVARIABLE %s AS %s", + varistransact, varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f7921f942d0..310701f09e1 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -719,6 +719,7 @@ typedef struct _VariableInfo const char *rolname; /* name of owner, or empty string */ bool varnotnull; bool varisimmutable; + bool varistransact; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 47cb59356e0..b39bddd3e40 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4077,6 +4077,42 @@ my %tests = ( }, }, + 'CREATE TRANSACTIONAL VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index cc69f07f1f9..8862539d76e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " NOT v.varnotnull as \"%s\",\n" " NOT v.varisimmutable as \"%s\",\n" + " v.varistransact as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5241,6 +5242,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Owner"), gettext_noop("Nullable"), gettext_noop("Mutable"), + gettext_noop("Transactional"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b653dda1fe8..e955522d2cb 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1305,6 +1305,8 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"TEXT SEARCH", NULL, NULL, NULL}, {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER}, + {"TRANSACTIONAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TRANSACTIONAL + * VARIABLE ... */ {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"}, {"TYPE", NULL, NULL, &Query_for_list_of_datatypes}, {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE @@ -3947,8 +3949,11 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ + else if (Matches("CREATE", "TRANSACTION|TRANSACTIONAL")) + COMPLETE_WITH("IMMUTABLE", "VARIABLE"); else if (TailMatches("CREATE", "VARIABLE", MatchAny) || TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("TRANSACTION|TRANSACTIONAL", "VARIABLE", MatchAny) || TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 68bc49a0e27..1dc44edf6e5 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -61,6 +61,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* don't allow changes */ bool varisimmutable BKI_DEFAULT(f); + /* supports transactions */ + bool varistransact BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bf820828dd9..a4372f184df 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3481,6 +3481,7 @@ typedef struct CreateSessionVarStmt bool if_not_exists; /* do nothing if it already exists */ bool not_null; /* disallow nulls */ bool is_immutable; /* don't allow changes */ + bool is_transact; /* supports transactions */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index aefe51f335b..125eb819b76 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -455,6 +455,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("to", TO, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("transactional", TRANSACTIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index e1635f686c2..da9f4a7030a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | t | t | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 5a66fc9ffab..3e6b73684fd 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1763,3 +1763,103 @@ SELECT var1; LET var1 = 30; ERROR: session variable "public.var1" is declared IMMUTABLE DROP VARIABLE var1; +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; +BEGIN; + LET tv = 100; + SELECT tv; + tv +----- + 100 +(1 row) + +ROLLBACK; +SELECT tv; + tv +---- + 0 +(1 row) + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 0; + SELECT tv; + tv +---- + 0 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +-- test subtransactions +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +COMMIT; +SELECT tv; + tv +---- + 1 +(1 row) + +DROP VARIABLE tv; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index fcd783e966c..68fc0cde44a 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1208,3 +1208,61 @@ SELECT var1; LET var1 = 30; DROP VARIABLE var1; + +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; + +BEGIN; + LET tv = 100; + SELECT tv; +ROLLBACK; + +SELECT tv; + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; + +SELECT tv; + +BEGIN; + LET tv = 0; + SELECT tv; +ROLLBACK; + +SELECT tv; + +-- test subtransactions + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +ROLLBACK; + +SELECT tv; + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +COMMIT; + +SELECT tv; + +DROP VARIABLE tv; -- 2.47.0 [text/x-patch] 0017-expression-with-session-variables-can-be-inlined.patch (4.2K, 5-0017-expression-with-session-variables-can-be-inlined.patch) download | inline diff: From b86f079f9ce82f62c7e92eab05c32c673c883b6d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 20:35:38 +0100 Subject: [PATCH 17/21] expression with session variables can be inlined There is not an reason why session variables should to block inlining. (of SQL functions). I can imagine some use cases like wrapping, and inlining significantly reduces an overhead of SQL functions. --- src/backend/optimizer/util/clauses.c | 37 ++++++++++++++----- .../regress/expected/session_variables.out | 7 ++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8cee732561d..f79937980ee 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -4744,8 +4744,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - (list_length(querytree->targetList) != 1) || - querytree->hasSessionVariables) + (list_length(querytree->targetList) != 1)) goto fail; /* If the function result is composite, resolve it */ @@ -4949,21 +4948,39 @@ substitute_actual_parameters_mutator(Node *node, { if (node == NULL) return NULL; + + + /* + * SQL functions can contain two different kind of params. The nodes with + * paramkind PARAM_EXTERN are related to function's arguments (and should + * be replaced in this step), because this is how we apply the function's + * arguments for an expression. + * + * The nodes with paramkind PARAM_VARIABLE are related to usage of session + * variables. The values of session variables are not passed to expression + * by expression arguments, so it should not be replaced here by + * function's arguments. + */ if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind != PARAM_EXTERN) + if (param->paramkind != PARAM_EXTERN && + param->paramkind != PARAM_VARIABLE) elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind); - if (param->paramid <= 0 || param->paramid > context->nargs) - elog(ERROR, "invalid paramid: %d", param->paramid); - /* Count usage of parameter */ - context->usecounts[param->paramid - 1]++; + if (param->paramkind == PARAM_EXTERN) + { + if (param->paramid <= 0 || param->paramid > context->nargs) + elog(ERROR, "invalid paramid: %d", param->paramid); - /* Select the appropriate actual arg and replace the Param with it */ - /* We don't need to copy at this time (it'll get done later) */ - return list_nth(context->args, param->paramid - 1); + /* Count usage of parameter */ + context->usecounts[param->paramid - 1]++; + + /* Select the appropriate actual arg and replace the Param with it */ + /* We don't need to copy at this time (it'll get done later) */ + return list_nth(context->args, param->paramid - 1); + } } return expression_tree_mutator(node, substitute_actual_parameters_mutator, (void *) context); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 8df3a91c05e..96283ed0b9a 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -105,7 +105,6 @@ SELECT var1; ERROR: permission denied for session variable var1 SELECT sqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN @@ -212,10 +211,10 @@ SELECT sqlfx1(sqlfx2('Hello')); -- inlining is blocked EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); - QUERY PLAN ------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------- Result - Output: sqlfx1(sqlfx2('Hello'::character varying)) + Output: ((var1 || ', '::text) || ((var2 || ', '::text) || 'Hello'::text)) (2 rows) DROP FUNCTION sqlfx1(varchar); -- 2.47.0 [text/x-patch] 0021-variable-fence-syntax-support-and-variable-fence-usa.patch (14.7K, 6-0021-variable-fence-syntax-support-and-variable-fence-usa.patch) download | inline diff: From 5c9b9608fe03eafd00950abf9fdf4e118f5d196b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 19 Nov 2024 08:14:53 +0100 Subject: [PATCH 21/21] variable fence syntax support and variable fence usage guard support this patch introduces a concept of variable fence - syntax for variable reference `VARIABLE(varname)` that is not in collision with column reference. When variable fence usage guard warning is active, then usage variable without variable fence in the case, where there can be column references, the the warning is raised. initial implementation of variable fence --- src/backend/parser/gram.y | 28 ++++++- src/backend/parser/parse_expr.c | 74 ++++++++++++++++++- src/backend/parser/parse_target.c | 7 ++ src/backend/utils/adt/ruleutils.c | 12 ++- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/nodes/parsenodes.h | 10 +++ src/include/nodes/primnodes.h | 2 + src/include/parser/parse_expr.h | 1 + .../regress/expected/session_variables.out | 56 ++++++++++++++ src/test/regress/sql/session_variables.sql | 35 +++++++++ 11 files changed, 228 insertions(+), 7 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af9d640c0e2..ad66eb450b4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -519,7 +519,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -879,7 +879,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15656,6 +15656,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17042,6 +17055,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 16bf52c9854..fecf22b8b9e 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -46,6 +46,7 @@ /* GUC parameters */ bool Transform_null_equals = false; bool session_variables_ambiguity_warning = false; +bool session_variables_use_fence_warning_guard = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -81,6 +82,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -112,7 +114,7 @@ static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); static Node *makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location); + char *attrname, bool fenced, int location); /* @@ -377,6 +379,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -1030,7 +1036,20 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) node = makeParamSessionVariable(pstate, varid, typid, typmod, collid, - attrname, cref->location); + attrname, false, cref->location); + + /* + * The variable is not inside variable's fence, so raise warning + * when variable fence guard is active and the query has FROM + * clause. + */ + if (session_variables_use_fence_warning_guard && pstate->p_rtable) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is not used inside variable fence", + NameListToString(cref->fields)), + errdetail("The collision of session variable' names and column names is possible."), + parser_errposition(pstate, cref->location))); } } } @@ -1073,7 +1092,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) static Node * makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location) + char *attrname, bool fenced, int location) { Param *param; @@ -1081,6 +1100,7 @@ makeParamSessionVariable(ParseState *pstate, param->paramkind = PARAM_VARIABLE; param->paramvarid = varid; + param->paramvarfenced = fenced; param->paramtype = typid; param->paramtypmod = typmod; param->paramcollid = collid; @@ -1156,6 +1176,54 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, true, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 6af6b801ad4..9d4adc0f5e9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2038,6 +2038,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index e7f74947cde..e218c7118b8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8622,8 +8622,16 @@ get_parameter(Param *param, deparse_context *context) /* translate paramvarid to session variable name */ if (param->paramkind == PARAM_VARIABLE) { - appendStringInfo(context->buf, "%s", - generate_session_variable_name(param->paramvarid)); + if (param->paramvarfenced) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + } + else + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + } return; } diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index efb65b02380..100dd26b286 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1604,6 +1604,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_use_fence_warning_guard", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when variable is not used inside variable fence."), + NULL + }, + &session_variables_use_fence_warning_guard, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 992dda3c644..512fe9e334d 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -715,6 +715,7 @@ #default_transaction_deferrable = off #session_replication_role = 'origin' #session_variables_ambiguity_warning = off +#session_variables_use_fence_warning_guard = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a4372f184df..97ed845e073 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -312,6 +312,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6174a5562c5..2e5665ffebd 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -388,6 +388,8 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of session variable if it is used */ Oid paramvarid; + /* true when variable is used inside an fence */ + bool paramvarfenced; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index e5aebf99b4a..d1f2e59d2dc 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -18,6 +18,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; extern PGDLLIMPORT bool session_variables_ambiguity_warning; +extern PGDLLIMPORT bool session_variables_use_fence_warning_guard; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 3e6b73684fd..66f1959a329 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1863,3 +1863,59 @@ SELECT tv; (1 row) DROP VARIABLE tv; +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; +CREATE VARIABLE a AS int; +LET a = 10; +CREATE TABLE test_table(a int, b int); +INSERT INTO test_table VALUES(20, 20); +-- no warning +SELECT a; + a +---- + 10 +(1 row) + +-- warning variable is shadowed +SELECT a, b FROM test_table; +WARNING: session variable "a" is shadowed +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a | b +----+---- + 20 | 20 +(1 row) + +-- no warning +SELECT variable(a) FROM test_table; + a +---- + 10 +(1 row) + +ALTER TABLE test_table DROP COLUMN a; +-- warning - variable fence is not used +SELECT a, b FROM test_table; +WARNING: session variable "a" is not used inside variable fence +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: The collision of session variable' names and column names is possible. + a | b +----+---- + 10 | 20 +(1 row) + +-- no warning +SELECT variable(a), b FROM test_table; + a | b +----+---- + 10 | 20 +(1 row) + +DROP VARIABLE a; +DROP TABLE test_table; +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 68fc0cde44a..45510e2ea54 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1266,3 +1266,38 @@ COMMIT; SELECT tv; DROP VARIABLE tv; + +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; + +CREATE VARIABLE a AS int; +LET a = 10; + +CREATE TABLE test_table(a int, b int); + +INSERT INTO test_table VALUES(20, 20); + +-- no warning +SELECT a; + +-- warning variable is shadowed +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a) FROM test_table; + +ALTER TABLE test_table DROP COLUMN a; + +-- warning - variable fence is not used +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a), b FROM test_table; + +DROP VARIABLE a; +DROP TABLE test_table; + +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; -- 2.47.0 [text/x-patch] 0018-this-patch-changes-error-message-column-doesn-t-exis.patch (29.2K, 7-0018-this-patch-changes-error-message-column-doesn-t-exis.patch) download | inline diff: From 9925a2ead22cf0b40f547ee665593bf4325ccf16 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:10:39 +0100 Subject: [PATCH 18/21] this patch changes error message "column doesn't exist" to message "column or variable doesn't exist" The error message will be more correct. Today, missing PL/pgSQL variable can be reported. The change has impact on lot of regress tests not directly related to session variables, and then it is distributed as separate patch --- src/backend/parser/parse_expr.c | 2 +- src/backend/parser/parse_relation.c | 24 +++++++++++--- src/backend/parser/parse_target.c | 8 +++-- src/include/parser/parse_expr.h | 2 ++ src/pl/plpgsql/src/expected/plpgsql_array.out | 2 +- .../plpgsql/src/expected/plpgsql_record.out | 4 +-- .../src/expected/plpgsql_session_variable.out | 2 +- src/pl/tcl/expected/pltcl_queries.out | 12 +++---- .../isolation/expected/session-variable.out | 4 +-- src/test/regress/expected/alter_table.out | 32 +++++++++---------- src/test/regress/expected/copy2.out | 2 +- src/test/regress/expected/errors.out | 8 ++--- src/test/regress/expected/join.out | 12 +++---- src/test/regress/expected/namespace.out | 2 +- src/test/regress/expected/numerology.out | 2 +- src/test/regress/expected/plpgsql.out | 12 +++---- src/test/regress/expected/psql.out | 2 +- src/test/regress/expected/rules.out | 2 +- .../regress/expected/session_variables.out | 6 ++-- src/test/regress/expected/transactions.out | 4 +-- src/test/regress/expected/union.out | 2 +- 21 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index bcbb1f564af..16bf52c9854 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -520,7 +520,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) * printed. When we are in an expression where session variables cannot be * used, we raise the first form of error message. */ -static bool +bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind) { bool result = false; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 95ae2108044..fa9a30bd138 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -27,6 +27,7 @@ #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_enr.h" +#include "parser/parse_expr.h" #include "parser/parse_relation.h" #include "parser/parse_type.h" #include "parser/parsetree.h" @@ -3730,6 +3731,19 @@ errorMissingRTE(ParseState *pstate, RangeVar *relation) parser_errposition(pstate, relation->location))); } +/* + * set message "column does not exist" or "column or variable does not exist" + * in dependency if expression context allows session variables. + */ +static int +column_or_variable_does_not_exists(ParseState *pstate, const char *colname) +{ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + return errmsg("column or variable \"%s\" does not exist", colname); + else + return errmsg("column \"%s\" does not exist", colname); +} + /* * Generate a suitable error about a missing column. * @@ -3764,7 +3778,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There are columns named \"%s\", but they are in tables that cannot be referenced from this part of the query.", colname), !relname ? errhint("Try using a table-qualified name.") : 0, @@ -3774,7 +3788,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There is a column named \"%s\" in table \"%s\", but it cannot be referenced from this part of the query.", colname, state->rexact1->eref->aliasname), rte_visible_if_lateral(pstate, state->rexact1) ? @@ -3792,14 +3806,14 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), parser_errposition(pstate, location))); /* Handle case where we have a single alternative spelling to offer */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, @@ -3813,7 +3827,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 76bf88c3ca2..6af6b801ad4 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -779,7 +779,9 @@ transformAssignmentIndirection(ParseState *pstate, if (!typrelid) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because its type %s is not a composite type" : + "cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); @@ -788,7 +790,9 @@ transformAssignmentIndirection(ParseState *pstate, if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because there is no such column in data type %s" : + "cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 0d64b300f8a..e5aebf99b4a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -23,4 +23,6 @@ extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKin extern const char *ParseExprKindName(ParseExprKind exprKind); +extern bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind); + #endif /* PARSE_EXPR_H */ diff --git a/src/pl/plpgsql/src/expected/plpgsql_array.out b/src/pl/plpgsql/src/expected/plpgsql_array.out index ad60e0e8be3..c1ab9fd7ed9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_array.out +++ b/src/pl/plpgsql/src/expected/plpgsql_array.out @@ -41,7 +41,7 @@ NOTICE: a = {"(,11)"}, a[1].i = 11 -- perhaps this ought to work, but for now it doesn't: do $$ declare a complex[]; begin a[1:2].i := array[11,12]; raise notice 'a = %', a; end$$; -ERROR: cannot assign to field "i" of column "a" because its type complex[] is not a composite type +ERROR: cannot assign to field "i" of column or variable "a" because its type complex[] is not a composite type LINE 1: a[1:2].i := array[11,12] ^ QUERY: a[1:2].i := array[11,12] diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index 6974c8f4a44..3970ac721c9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -135,7 +135,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ declare c nested_int8s; begin c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "c" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "c" because there is no such column in data type two_int8s LINE 1: c.c2.x = 1 ^ QUERY: c.c2.x = 1 @@ -157,7 +157,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "b.c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ <<b>> declare c nested_int8s; begin b.c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "b" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "b" because there is no such column in data type two_int8s LINE 1: b.c.c2.x = 1 ^ QUERY: b.c.c2.x = 1 diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ecac64fcfb4..8bd2b398bdd 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -232,7 +232,7 @@ SET ROLE TO DEFAULT; SET ROLE TO regress_var_exec_role; -- should fail, but not crash SELECT var_read_func(); -ERROR: column "plpgsql_sv_var1" does not exist +ERROR: column or variable "plpgsql_sv_var1" does not exist LINE 1: plpgsql_sv_var1 ^ QUERY: plpgsql_sv_var1 diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out index 35cc6e62aad..497cd38189b 100644 --- a/src/pl/tcl/expected/pltcl_queries.out +++ b/src/pl/tcl/expected/pltcl_queries.out @@ -450,12 +450,12 @@ CONTEXT: while executing "__PLTcl_proc_tcl_eval_text spi_prepare\ a\ \"b\ \{\"" in PL/Tcl function tcl_eval(text) select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$); - tcl_error_handling_test --------------------------------------- - SQLSTATE: 42703 + - condition: undefined_column + - cursor_position: 8 + - message: column "moo" does not exist+ + tcl_error_handling_test +-------------------------------------------------- + SQLSTATE: 42703 + + condition: undefined_column + + cursor_position: 8 + + message: column or variable "moo" does not exist+ statement: select moo (1 row) diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index e9a254bcd12..d8848d555cd 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -10,7 +10,7 @@ test step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist starting permutation: let val s1 drop val sr1 step let: LET myvar = 'test'; @@ -23,7 +23,7 @@ test step s1: BEGIN; step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist step sr1: ROLLBACK; starting permutation: let val dbg drop create dbg val diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 2212c8dbb59..35b3a3bd302 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1295,19 +1295,19 @@ select * from atacc1; (1 row) select * from atacc1 order by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 order by a; ^ select * from atacc1 order by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 order by "........pg.dropped.1........"... ^ select * from atacc1 group by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 group by a; ^ select * from atacc1 group by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 group by "........pg.dropped.1........"... ^ select atacc1.* from atacc1; @@ -1317,7 +1317,7 @@ select atacc1.* from atacc1; (1 row) select a from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a from atacc1; ^ select atacc1.a from atacc1; @@ -1331,15 +1331,15 @@ select b,c,d from atacc1; (1 row) select a,b,c,d from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a,b,c,d from atacc1; ^ select * from atacc1 where a = 1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 where a = 1; ^ select "........pg.dropped.1........" from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........" from atacc1; ^ select atacc1."........pg.dropped.1........" from atacc1; @@ -1347,11 +1347,11 @@ ERROR: column atacc1.........pg.dropped.1........ does not exist LINE 1: select atacc1."........pg.dropped.1........" from atacc1; ^ select "........pg.dropped.1........",b,c,d from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........",b,c,d from atacc1; ^ select * from atacc1 where "........pg.dropped.1........" = 1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 where "........pg.dropped.1........" = ... ^ -- UPDATEs @@ -1360,7 +1360,7 @@ ERROR: column "a" of relation "atacc1" does not exist LINE 1: update atacc1 set a = 3; ^ update atacc1 set b = 2 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: update atacc1 set b = 2 where a = 3; ^ update atacc1 set "........pg.dropped.1........" = 3; @@ -1368,7 +1368,7 @@ ERROR: column "........pg.dropped.1........" of relation "atacc1" does not exis LINE 1: update atacc1 set "........pg.dropped.1........" = 3; ^ update atacc1 set b = 2 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: update atacc1 set b = 2 where "........pg.dropped.1........"... ^ -- INSERTs @@ -1416,11 +1416,11 @@ LINE 1: insert into atacc1 ("........pg.dropped.1........",b,c,d) va... ^ -- DELETEs delete from atacc1 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: delete from atacc1 where a = 3; ^ delete from atacc1 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: delete from atacc1 where "........pg.dropped.1........" = 3; ^ delete from atacc1; @@ -1706,7 +1706,7 @@ select f1 from c1; alter table c1 drop column f1; select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". @@ -1720,7 +1720,7 @@ ERROR: cannot drop inherited column "f1" alter table p1 drop column f1; -- c1.f1 is dropped now, since there is no local definition for it select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 64ea33aeae8..6874f5ae0d1 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -160,7 +160,7 @@ LINE 1: COPY x TO stdout WHERE a = 1; COPY x from stdin WHERE a = 50004; COPY x from stdin WHERE a > 60003; COPY x from stdin WHERE f > 60003; -ERROR: column "f" does not exist +ERROR: column or variable "f" does not exist LINE 1: COPY x from stdin WHERE f > 60003; ^ COPY x from stdin WHERE a = max(x.b); diff --git a/src/test/regress/expected/errors.out b/src/test/regress/expected/errors.out index 8c527474daf..e53ae451dfc 100644 --- a/src/test/regress/expected/errors.out +++ b/src/test/regress/expected/errors.out @@ -27,7 +27,7 @@ LINE 1: select * from nonesuch; ^ -- bad name in target list select nonesuch from pg_database; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select nonesuch from pg_database; ^ -- empty distinct list isn't OK @@ -37,17 +37,17 @@ LINE 1: select distinct from pg_database; ^ -- bad attribute name on lhs of operator select * from pg_database where nonesuch = pg_database.datname; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select * from pg_database where nonesuch = pg_database.datna... ^ -- bad attribute name on rhs of operator select * from pg_database where pg_database.datname = nonesuch; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: ...ect * from pg_database where pg_database.datname = nonesuch; ^ -- bad attribute name in select distinct on select distinct on (foobar) * from pg_database; -ERROR: column "foobar" does not exist +ERROR: column or variable "foobar" does not exist LINE 1: select distinct on (foobar) * from pg_database; ^ -- grouping with FOR UPDATE diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 9b2973694ff..d9f16aeb978 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6310,13 +6310,13 @@ LINE 1: select t2.uunique1 from HINT: Perhaps you meant to reference the column "t2.unique1". select uunique1 from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once -ERROR: column "uunique1" does not exist +ERROR: column or variable "uunique1" does not exist LINE 1: select uunique1 from ^ HINT: Perhaps you meant to reference the column "t1.unique1" or the column "t2.unique1". select ctid from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, need qualification -ERROR: column "ctid" does not exist +ERROR: column or variable "ctid" does not exist LINE 1: select ctid from ^ DETAIL: There are columns named "ctid", but they are in tables that cannot be referenced from this part of the query. @@ -7413,7 +7413,7 @@ lateral (select * from int8_tbl t1, -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7425,7 +7425,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss; DETAIL: There is an entry for table "a", but it cannot be referenced from this part of the query. HINT: To reference that table, you must mark this subquery with LATERAL. select f1,g from int4_tbl a cross join (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7462,7 +7462,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i... create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl; -- error, can't do this: update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. @@ -7482,7 +7482,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1) ERROR: table name "xx1" specified more than once -- also errors: delete from xx1 using (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/namespace.out b/src/test/regress/expected/namespace.out index dbbda72d395..d98793f92a7 100644 --- a/src/test/regress/expected/namespace.out +++ b/src/test/regress/expected/namespace.out @@ -23,7 +23,7 @@ BEGIN; SET search_path to public, test_ns_schema_1; CREATE SCHEMA test_ns_schema_2 CREATE VIEW abc_view AS SELECT c FROM abc; -ERROR: column "c" does not exist +ERROR: column or variable "c" does not exist LINE 2: CREATE VIEW abc_view AS SELECT c FROM abc; ^ COMMIT; diff --git a/src/test/regress/expected/numerology.out b/src/test/regress/expected/numerology.out index 9e23166fed1..672012e1461 100644 --- a/src/test/regress/expected/numerology.out +++ b/src/test/regress/expected/numerology.out @@ -314,7 +314,7 @@ NOTICE: i = 1002 NOTICE: i = 1003 -- error cases SELECT _100; -ERROR: column "_100" does not exist +ERROR: column or variable "_100" does not exist LINE 1: SELECT _100; ^ SELECT 100_; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 0a6945581bd..5c599ba312e 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -2598,7 +2598,7 @@ end; $$ language plpgsql; -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION -- blocks select excpt_test1(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -2613,7 +2613,7 @@ begin end; $$ language plpgsql; -- should fail select excpt_test2(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -4675,7 +4675,7 @@ BEGIN RAISE NOTICE '%, %', r.roomno, r.comment; END LOOP; END$$; -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn... ^ QUERY: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno @@ -4717,7 +4717,7 @@ begin raise notice 'x = %', x; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -4729,7 +4729,7 @@ begin raise notice 'x = %, y = %', x, y; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -5769,7 +5769,7 @@ ALTER TABLE alter_table_under_transition_tables DROP column name; UPDATE alter_table_under_transition_tables SET id = id; -ERROR: column "name" does not exist +ERROR: column or variable "name" does not exist LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d) ^ QUERY: (SELECT string_agg(id || '=' || name, ',') FROM d) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 77a7bf45c8a..e1635f686c2 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -214,7 +214,7 @@ select $1::int as col \bind 1 \g \bind 2 \g -- errors -- parse error SELECT foo \bind \g -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT foo ^ -- tcop error diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 3014d047fef..7378f95b17d 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo; -- this should fail because f1 is not exposed for unqualified reference: create rule rules_foorule as on insert to rules_foo where f1 < 100 do instead insert into rules_foo2 values (f1); -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 2: do instead insert into rules_foo2 values (f1); ^ DETAIL: There are columns named "f1", but they are in tables that cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 96283ed0b9a..5a66fc9ffab 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -310,7 +310,7 @@ ERROR: session variable "var1" doesn't exist LINE 1: LET var1 = pi(); ^ SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ -- should be ok @@ -522,7 +522,7 @@ SELECT v1; -- should fail, attribute doesn't exist LET v1.x = 10; -ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +ERROR: cannot assign to field "x" of column or variable "v1" because there is no such column in data type t1 LINE 1: LET v1.x = 10; ^ -- should fail, don't allow multi column query @@ -1070,7 +1070,7 @@ BEGIN; DROP VARIABLE var1; -- should fail SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ ROLLBACK; diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out index 7f5757e89c4..b05b16d94bf 100644 --- a/src/test/regress/expected/transactions.out +++ b/src/test/regress/expected/transactions.out @@ -256,7 +256,7 @@ SELECT * FROM trans_barbaz; -- should have 1 BEGIN; SAVEPOINT one; SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ ROLLBACK TO SAVEPOINT one; @@ -305,7 +305,7 @@ BEGIN; SAVEPOINT one; INSERT INTO savepoints VALUES (5); SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ COMMIT; diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index c73631a9a1d..5c0eacc5f15 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -922,7 +922,7 @@ ORDER BY q2,q1; -- This should fail, because q2 isn't a name of an EXCEPT output column SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1; -ERROR: column "q2" does not exist +ERROR: column or variable "q2" does not exist LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1... ^ DETAIL: There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query. -- 2.47.0 [text/x-patch] 0016-plpgsql-implementation-for-LET-statement.patch (14.2K, 8-0016-plpgsql-implementation-for-LET-statement.patch) download | inline diff: From 6abaec095d372050fd27fb91ab5afb2fd1519125 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 08:56:17 +0100 Subject: [PATCH 16/21] plpgsql implementation for LET statement PLpgSQL allows to call expression executor directly (for simple expression). This possibility strongly reduces overhead related to execution. Reimplementation of LET statement (inside PLpgSQL) allows to use this possibility, and strongly increase performance: CREATE VARIABLE svar int; DO $$ BEGIN FOR i IN 1..10000 LOOP LET svar = i; END LOOP; END; $$; From 120ms to 8ms (with assertions) (this is best case, but without it, the LET statement can be bottle neck). An alternative can be reimplementation of LET statement inside expression executor, and then SQL LET command can be executed in simple expression execution, but this increase an complexity of executor, but still the benefits is only inside plpgsql, so it is better to this optimization inside plpgsql. --- src/backend/executor/spi.c | 3 ++ src/backend/parser/analyze.c | 12 +++++ src/backend/parser/gram.y | 8 ++++ src/backend/parser/parser.c | 1 + src/include/nodes/parsenodes.h | 2 + src/include/parser/parser.h | 4 ++ src/pl/plpgsql/src/pl_exec.c | 55 +++++++++++++++++++++++ src/pl/plpgsql/src/pl_funcs.c | 24 ++++++++++ src/pl/plpgsql/src/pl_gram.y | 28 +++++++++++- src/pl/plpgsql/src/pl_reserved_kwlist.h | 1 + src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 + src/pl/plpgsql/src/plpgsql.h | 12 +++++ src/tools/pgindent/typedefs.list | 1 + 13 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 2fb2e73604e..5ae25d07ad7 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2991,6 +2991,9 @@ _SPI_error_callback(void *arg) case RAW_PARSE_PLPGSQL_ASSIGN3: errcontext("PL/pgSQL assignment \"%s\"", query); break; + case RAW_PARSE_PLPGSQL_LET: + errcontext("LET statement \"%s\"", query); + break; default: errcontext("SQL statement \"%s\"", query); break; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 496d75a494a..2edc51cfee9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -2014,6 +2014,18 @@ transformLetStmt(ParseState *pstate, LetStmt *stmt) stmt->query = (Node *) query; + /* + * Inside PL/pgSQL we don't want to execute LET statement as utility + * command, because it disallow to execute expression as simple + * expression. So for PL/pgSQL we have extra path, and we return SELECT. + * Then it can be executed by exec_eval_expr. Result is dirrectly assigned + * to target session variable inside PL/pgSQL LET statement handler. This + * is extra code, extra path, but possibility to get faster execution is + * too attractive. + */ + if (stmt->plpgsql_mode) + return query; + /* represent the command as a utility Query */ result = makeNode(Query); result->commandType = CMD_UTILITY; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6aea3bd35b2..bc8e25b1933 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -817,6 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %token MODE_PLPGSQL_ASSIGN1 %token MODE_PLPGSQL_ASSIGN2 %token MODE_PLPGSQL_ASSIGN3 +%token MODE_PLPGSQL_LET /* Precedence: lowest to highest */ @@ -948,6 +949,13 @@ parse_toplevel: pg_yyget_extra(yyscanner)->parsetree = list_make1(makeRawStmt((Node *) n, @2)); } + | MODE_PLPGSQL_LET LetStmt + { + LetStmt *n = (LetStmt *) $2; + n->plpgsql_mode = true; + pg_yyget_extra(yyscanner)->parsetree = + list_make1(makeRawStmt((Node *) n, 0)); + } ; /* diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c index 118488c3f30..f9430a32919 100644 --- a/src/backend/parser/parser.c +++ b/src/backend/parser/parser.c @@ -62,6 +62,7 @@ raw_parser(const char *str, RawParseMode mode) [RAW_PARSE_PLPGSQL_ASSIGN1] = MODE_PLPGSQL_ASSIGN1, [RAW_PARSE_PLPGSQL_ASSIGN2] = MODE_PLPGSQL_ASSIGN2, [RAW_PARSE_PLPGSQL_ASSIGN3] = MODE_PLPGSQL_ASSIGN3, + [RAW_PARSE_PLPGSQL_LET] = MODE_PLPGSQL_LET, }; yyextra.have_lookahead = true; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7308c323574..bf820828dd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2114,6 +2114,8 @@ typedef struct LetStmt NodeTag type; List *target; /* target variable */ Node *query; /* source expression */ + bool plpgsql_mode; /* true, when command will be executed + * (parsed) by plpgsql runtime */ int location; } LetStmt; diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h index be184ec5066..efbc76f0ad4 100644 --- a/src/include/parser/parser.h +++ b/src/include/parser/parser.h @@ -33,6 +33,9 @@ * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement, * and return a one-element List containing a RawStmt node. "n" * gives the number of dotted names comprising the target ColumnRef. + * + * RAW_PARSE_PLPGSQL_LET: parse a LET statement, and return a + * one-element List containing a RawStmt node. */ typedef enum { @@ -42,6 +45,7 @@ typedef enum RAW_PARSE_PLPGSQL_ASSIGN1, RAW_PARSE_PLPGSQL_ASSIGN2, RAW_PARSE_PLPGSQL_ASSIGN3, + RAW_PARSE_PLPGSQL_LET, } RawParseMode; /* Values for the backslash_quote GUC */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..b5cb1bee223 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -22,6 +22,7 @@ #include "access/tupconvert.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/spi.h" #include "executor/tstoreReceiver.h" @@ -323,6 +324,8 @@ static int exec_stmt_commit(PLpgSQL_execstate *estate, PLpgSQL_stmt_commit *stmt); static int exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt); +static int exec_stmt_let(PLpgSQL_execstate *estate, + PLpgSQL_stmt_let *let); static void plpgsql_estate_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, @@ -2116,6 +2119,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts) rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt); + break; + default: /* point err_stmt to parent, since this one seems corrupt */ estate->err_stmt = save_estmt; @@ -4999,6 +5006,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt) return PLPGSQL_RC_OK; } +/* ---------- + * exec_stmt_let Evaluate an expression and + * put the result into a session variable. + * ---------- + */ +static int +exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt) +{ + bool isNull; + Oid valtype; + int32 valtypmod; + Datum value; + Oid varid; + + List *plansources; + CachedPlanSource *plansource; + + value = exec_eval_expr(estate, + stmt->expr, + &isNull, + &valtype, + &valtypmod); + + /* + * Oid of target session variable is stored in Query structure. It is + * safer to read this value directly from the plan than to hold this value + * in the plpgsql context, because it's not necessary to handle + * invalidation of the cached value. Next operations are read only without + * any allocations, so we can expect that retrieving varid from Query + * should be fast. + */ + plansources = SPI_plan_get_plan_sources(stmt->expr->plan); + if (list_length(plansources) != 1) + elog(ERROR, "unexpected length of plansources of query for LET statement"); + + plansource = (CachedPlanSource *) linitial(plansources); + if (list_length(plansource->query_list) != 1) + elog(ERROR, "unexpected length of plansource of query for LET statement"); + + varid = linitial_node(Query, plansource->query_list)->resultVariable; + if (!OidIsValid(varid)) + elog(ERROR, "oid of target session variable is not valid"); + + SetSessionVariableWithSecurityCheck(varid, value, isNull); + + return PLPGSQL_RC_OK; +} + /* ---------- * exec_assign_expr Put an expression's result into a variable. * ---------- diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index eeb7c4d7c04..1b39351db7f 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt) return "COMMIT"; case PLPGSQL_STMT_ROLLBACK: return "ROLLBACK"; + case PLPGSQL_STMT_LET: + return "LET"; } return "unknown"; @@ -370,6 +372,7 @@ static void free_perform(PLpgSQL_stmt_perform *stmt); static void free_call(PLpgSQL_stmt_call *stmt); static void free_commit(PLpgSQL_stmt_commit *stmt); static void free_rollback(PLpgSQL_stmt_rollback *stmt); +static void free_let(PLpgSQL_stmt_let *stmt); static void free_expr(PLpgSQL_expr *expr); @@ -459,6 +462,9 @@ free_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: free_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + free_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -713,6 +719,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt) { } +static void +free_let(PLpgSQL_stmt_let *stmt) +{ + free_expr(stmt->expr); +} + static void free_expr(PLpgSQL_expr *expr) { @@ -815,6 +827,7 @@ static void dump_perform(PLpgSQL_stmt_perform *stmt); static void dump_call(PLpgSQL_stmt_call *stmt); static void dump_commit(PLpgSQL_stmt_commit *stmt); static void dump_rollback(PLpgSQL_stmt_rollback *stmt); +static void dump_let(PLpgSQL_stmt_let *stmt); static void dump_expr(PLpgSQL_expr *expr); @@ -914,6 +927,9 @@ dump_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: dump_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + dump_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -1590,6 +1606,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt) printf("\n"); } +static void +dump_let(PLpgSQL_stmt_let *stmt) +{ + dump_ind(); + dump_expr(stmt->expr); + printf("\n"); +} + static void dump_expr(PLpgSQL_expr *expr) { diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 8182ce28aa1..3f155c4f8a4 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -200,7 +200,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %type <stmt> stmt_return stmt_raise stmt_assert stmt_execsql %type <stmt> stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag %type <stmt> stmt_open stmt_fetch stmt_move stmt_close stmt_null -%type <stmt> stmt_commit stmt_rollback +%type <stmt> stmt_commit stmt_rollback stmt_let %type <stmt> stmt_case stmt_foreach_a %type <list> proc_exceptions @@ -307,6 +307,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %token <keyword> K_INTO %token <keyword> K_IS %token <keyword> K_LAST +%token <keyword> K_LET %token <keyword> K_LOG %token <keyword> K_LOOP %token <keyword> K_MERGE @@ -876,6 +877,8 @@ proc_stmt : pl_block ';' { $$ = $1; } | stmt_rollback { $$ = $1; } + | stmt_let + { $$ = $1; } ; stmt_perform : K_PERFORM @@ -992,6 +995,29 @@ stmt_assign : T_DATUM } ; +stmt_let : K_LET + { + PLpgSQL_stmt_let *new; + RawParseMode pmode; + + pmode = RAW_PARSE_PLPGSQL_LET; + + new = palloc0(sizeof(PLpgSQL_stmt_let)); + new->cmd_type = PLPGSQL_STMT_LET; + new->lineno = plpgsql_location_to_lineno(@1); + new->stmtid = ++plpgsql_curr_compile->nstatements; + + /* push back the head name to include it in the stmt */ + plpgsql_push_back_token(K_LET); + new->expr = read_sql_construct(';', 0, 0, ";", + pmode, + false, true, + NULL, NULL); + + $$ = (PLpgSQL_stmt *)new; + } + ; + stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' { PLpgSQL_stmt_getdiag *new; diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h index d338e9f6374..be94aa751a4 100644 --- a/src/pl/plpgsql/src/pl_reserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h @@ -40,6 +40,7 @@ PG_KEYWORD("from", K_FROM) PG_KEYWORD("if", K_IF) PG_KEYWORD("in", K_IN) PG_KEYWORD("into", K_INTO) +PG_KEYWORD("let", K_LET) PG_KEYWORD("loop", K_LOOP) PG_KEYWORD("not", K_NOT) PG_KEYWORD("null", K_NULL) diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h index 670b4cf0c1e..602628d7d2a 100644 --- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h @@ -69,6 +69,7 @@ PG_KEYWORD("info", K_INFO) PG_KEYWORD("insert", K_INSERT) PG_KEYWORD("is", K_IS) PG_KEYWORD("last", K_LAST) +PG_KEYWORD("let", K_LET) PG_KEYWORD("log", K_LOG) PG_KEYWORD("merge", K_MERGE) PG_KEYWORD("message", K_MESSAGE) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 50c3b28472b..bb400629adb 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -128,6 +128,7 @@ typedef enum PLpgSQL_stmt_type PLPGSQL_STMT_CALL, PLPGSQL_STMT_COMMIT, PLPGSQL_STMT_ROLLBACK, + PLPGSQL_STMT_LET, } PLpgSQL_stmt_type; /* @@ -520,6 +521,17 @@ typedef struct PLpgSQL_stmt_assign PLpgSQL_expr *expr; } PLpgSQL_stmt_assign; +/* + * Let statement + */ +typedef struct PLpgSQL_stmt_let +{ + PLpgSQL_stmt_type cmd_type; + int lineno; + unsigned int stmtid; + PLpgSQL_expr *expr; +} PLpgSQL_stmt_let; + /* * PERFORM statement */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 64ddf589b3c..fe60f2916f4 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1890,6 +1890,7 @@ PLpgSQL_stmt_forq PLpgSQL_stmt_fors PLpgSQL_stmt_getdiag PLpgSQL_stmt_if +PLpgSQL_stmt_let PLpgSQL_stmt_loop PLpgSQL_stmt_open PLpgSQL_stmt_perform -- 2.47.0 [text/x-patch] 0014-allow-read-an-value-of-session-variable-directly-fro.patch (13.3K, 9-0014-allow-read-an-value-of-session-variable-directly-fro.patch) download | inline diff: From 3456ed8963f41c59ff32426c01f2506913c0fc36 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:07:03 +0100 Subject: [PATCH 14/21] allow read an value of session variable directly from expression executor The expression executor can be called directly (not inside query executor) when expression is evaluated as simple expression in plpgsql or when expression is an argument of CALL or EXECUTE statements. For these cases we need to allow direct access to content of session variables from executor. We can do it, because we know so the expression is not evaluated under query and cannot be evaluated inside parallel worker. This patch enables session variables for simple expression evaluation. This patch enables usage session variables in arguments of CALL stmt. --- src/backend/commands/prepare.c | 8 -- src/backend/executor/execExpr.c | 82 ++++++++++++++----- src/backend/executor/execExprInterp.c | 16 ++++ src/backend/jit/llvm/llvmjit_expr.c | 6 ++ src/backend/parser/analyze.c | 8 -- src/include/executor/execExpr.h | 12 ++- .../src/expected/plpgsql_session_variable.out | 3 +- src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 26 +++--- src/test/regress/sql/session_variables.sql | 12 +-- 10 files changed, 117 insertions(+), 59 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 3735b5554e0..e8c375df198 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,14 +338,6 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } - /* - * The arguments of EXECUTE are evaluated by a direct expression - * executor call. This mode doesn't support session variables yet. - * It will be enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6dc8c562bec..845bdd1ae85 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1005,25 +1005,69 @@ ExecInitExprRec(Expr *node, ExprState *state, es_num_session_variables = state->parent->state->es_num_session_variables; } - Assert(es_session_variables); - - /* parameter sanity checks */ - if (param->paramid >= es_num_session_variables) - elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); - - var = &es_session_variables[param->paramid]; - - if (var->typid != param->paramtype) - elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); - - /* - * In this case, pass the value like a - * constant. - */ - scratch.opcode = EEOP_CONST; - scratch.d.constval.value = var->value; - scratch.d.constval.isnull = var->isnull; - ExprEvalPushStep(state, &scratch); + if (es_session_variables) + { + /* + * This path is used when expression is + * evaluated inside query evaluation. For + * ensuring of immutability of session variable + * inside query we use special buffer with copy + * of used session variables. This method helps + * with parallel execution too. + */ + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + else + { + AclResult aclresult; + Oid varid = param->paramvarid; + Oid vartype = param->paramtype; + + Assert(!IsParallelWorker()); + + /* + * In some cases (plpgsql's simple expression + * evaluation or evaluation of CALL arguments), + * the expression executor is called directly. + * We can allow direct access to session + * variables (copy only), because we know, so + * outside is not any query (and expression + * cannot be executed parallel). + * + * In this case we should to do aclcheck, + * because usual aclcheck from + * standard_ExecutorStart is not executed in + * this case. Fortunately it is just once per + * transaction. + */ + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + scratch.opcode = EEOP_PARAM_VARIABLE; + scratch.d.vparam.varid = varid; + scratch.d.vparam.vartype = vartype; + ExprEvalPushStep(state, &scratch); + } } break; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 30c5a19aad6..0a154b97d6e 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -59,6 +59,7 @@ #include "access/heaptoast.h" #include "catalog/pg_type.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -450,6 +451,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_PARAM_EXEC, &&CASE_EEOP_PARAM_EXTERN, &&CASE_EEOP_PARAM_CALLBACK, + &&CASE_EEOP_PARAM_VARIABLE, &&CASE_EEOP_PARAM_SET, &&CASE_EEOP_CASE_TESTVAL, &&CASE_EEOP_MAKE_READONLY, @@ -1099,6 +1101,20 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_PARAM_VARIABLE) + { + /* + * Direct access to session variable (without buffering). Because + * returned value can be used (without an assignement) after the + * referenced session variables is updated, we have to use an copy + * of stored value every time. + */ + *op->resvalue = GetSessionVariableWithTypeCheck(op->d.vparam.varid, + op->resnull, + op->d.vparam.vartype); + EEO_NEXT(); + } + EEO_CASE(EEOP_PARAM_SET) { /* out of line, unlikely to matter performance-wise */ diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 9cec17544b8..e8cc54351ed 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1136,6 +1136,12 @@ llvm_compile_expr(ExprState *state) break; } + case EEOP_PARAM_VARIABLE: + build_EvalXFunc(b, mod, "ExecEvalParamVariable", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_PARAM_SET: build_EvalXFunc(b, mod, "ExecEvalParamSet", v_state, op, v_econtext); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 9dfd435128a..496d75a494a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3470,14 +3470,6 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); - /* - * The arguments of CALL statement are evaluated by a direct expression - * executor call. This path is unsupported yet, so block it. It will be - * enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index cd97dfa0624..72b54b3a33f 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -156,10 +156,11 @@ typedef enum ExprEvalOp EEOP_BOOLTEST_IS_FALSE, EEOP_BOOLTEST_IS_NOT_FALSE, - /* evaluate PARAM_EXEC/EXTERN parameters */ + /* evaluate PARAM_EXEC/EXTERN/VARIABLE parameters */ EEOP_PARAM_EXEC, EEOP_PARAM_EXTERN, EEOP_PARAM_CALLBACK, + EEOP_PARAM_VARIABLE, /* set PARAM_EXEC value */ EEOP_PARAM_SET, @@ -409,6 +410,13 @@ typedef struct ExprEvalStep Oid paramtype; /* OID of parameter's datatype */ } cparam; + /* for EEOP_PARAM_VARIABLE */ + struct + { + Oid varid; /* OID of assigned variable */ + Oid vartype; /* OID of parameter's datatype */ + } vparam; + /* for EEOP_CASE_TESTVAL/DOMAIN_TESTVAL */ struct { @@ -831,6 +839,8 @@ extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op); extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op); extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index a6523f7afe2..ecac64fcfb4 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -211,8 +211,7 @@ SET ROLE TO regress_var_exec_role; -- should fail SELECT var_read_func(); ERROR: permission denied for session variable plpgsql_sv_var1 -CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" -PL/pgSQL function var_read_func() line 3 at RAISE +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE SET ROLE TO DEFAULT; SET ROLE TO regress_var_owner_role; GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 1361a8a8480..86c5bd324a9 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,8 +8099,7 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations || - query->hasSessionVariables) + query->setOperations) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a8520e5579d..2f1708f7c84 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -108,8 +108,7 @@ ERROR: permission denied for session variable var1 CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: PL/pgSQL expression "$1 + var1" -PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN -- should be ok SELECT sqlfx_sd(20); sqlfx_sd @@ -279,27 +278,28 @@ $$; NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); -ERROR: session variable cannot be used as an argument +NOTICE: 1 DO $$ BEGIN CALL p(v); END $$; -ERROR: session variable cannot be used as an argument -CONTEXT: SQL statement "CALL p(v)" -PL/pgSQL function inline_code_block line 1 at CALL +NOTICE: 1 DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); -ERROR: session variable cannot be used as an argument + ?column? +---------- + 20 +(1 row) + DEALLOCATE ptest; DROP VARIABLE v; -- test search path diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index ac8e2cb0943..fcd783e966c 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -207,13 +207,14 @@ $$; DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; + CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); DO $$ BEGIN CALL p(v); END $$; @@ -221,13 +222,12 @@ DO $$ BEGIN CALL p(v); END $$; DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); DEALLOCATE ptest; -- 2.47.0 [text/x-patch] 0015-allow-parallel-execution-queries-with-session-variab.patch (11.9K, 10-0015-allow-parallel-execution-queries-with-session-variab.patch) download | inline diff: From 644d333fca884762d32b182b8fd8883fddab7de9 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:08:17 +0100 Subject: [PATCH 15/21] allow parallel execution queries with session variables --- doc/src/sgml/parallel.sgml | 6 - src/backend/executor/execMain.c | 14 +- src/backend/executor/execParallel.c | 147 +++++++++++++++++- src/backend/optimizer/util/clauses.c | 18 +-- .../regress/expected/session_variables.out | 12 +- 5 files changed, 171 insertions(+), 26 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 683dede6adc..1ce9abf86f5 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,12 +515,6 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> - - <listitem> - <para> - Plan nodes that use a session variable. - </para> - </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 21e938a6227..783716c4e57 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -212,7 +212,19 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) * be changed inside query execution time, and then a reference to * previously returned value can be corrupted). */ - if (queryDesc->plannedstmt->sessionVariables) + if (queryDesc->num_session_variables > 0) + { + /* + * When a parallel query needs to access query parameters (including + * related session variables), then related session variables are + * restored (deserialized) in queryDesc already. So just push pointer + * of this array to executor's estate. + */ + Assert(IsParallelWorker()); + estate->es_session_variables = queryDesc->session_variables; + estate->es_num_session_variables = queryDesc->num_session_variables; + } + else if (queryDesc->plannedstmt->sessionVariables) { ListCell *lc; int nSessionVariables; diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c index bfb3419efb7..cb434e2768e 100644 --- a/src/backend/executor/execParallel.c +++ b/src/backend/executor/execParallel.c @@ -12,8 +12,9 @@ * workers and ensuring that their state generally matches that of the * leader; see src/backend/access/transam/README.parallel for details. * However, we must save and restore relevant executor state, such as - * any ParamListInfo associated with the query, buffer/WAL usage info, and - * the actual plan to be passed down to the worker. + * any ParamListInfo associated with the query, buffer/WAL usage info, + * session variables buffer, and the actual plan to be passed down to + * the worker. * * IDENTIFICATION * src/backend/executor/execParallel.c @@ -64,6 +65,7 @@ #define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xE000000000000008) #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009) #define PARALLEL_KEY_WAL_USAGE UINT64CONST(0xE00000000000000A) +#define PARALLEL_KEY_SESSION_VARIABLES UINT64CONST(0xE00000000000000B) #define PARALLEL_TUPLE_QUEUE_SIZE 65536 @@ -138,6 +140,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate, /* Helper function that runs in the parallel worker. */ static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc); +/* Helper functions that can pass values of session variables */ +static Size EstimateSessionVariables(EState *estate); +static void SerializeSessionVariables(EState *estate, char **start_address); +static SessionVariableValue *RestoreSessionVariables(char **start_address, + int *num_session_variables); + /* * Create a serialized representation of the plan to be sent to each worker. */ @@ -596,6 +604,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, char *pstmt_data; char *pstmt_space; char *paramlistinfo_space; + char *session_variables_space; BufferUsage *bufusage_space; WalUsage *walusage_space; SharedExecutorInstrumentation *instrumentation = NULL; @@ -605,6 +614,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, int instrumentation_len = 0; int jit_instrumentation_len = 0; int instrument_offset = 0; + int session_variables_len = 0; Size dsa_minsize = dsa_minimum_size(); char *query_string; int query_len; @@ -660,6 +670,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len); shm_toc_estimate_keys(&pcxt->estimator, 1); + /* Estimate space for serialized session variables. */ + session_variables_len = EstimateSessionVariables(estate); + shm_toc_estimate_chunk(&pcxt->estimator, session_variables_len); + shm_toc_estimate_keys(&pcxt->estimator, 1); + /* * Estimate space for BufferUsage. * @@ -761,6 +776,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space); SerializeParamList(estate->es_param_list_info, ¶mlistinfo_space); + /* Store serialized session variables. */ + session_variables_space = shm_toc_allocate(pcxt->toc, session_variables_len); + shm_toc_insert(pcxt->toc, PARALLEL_KEY_SESSION_VARIABLES, session_variables_space); + SerializeSessionVariables(estate, &session_variables_space); + /* Allocate space for each worker's BufferUsage; no need to initialize. */ bufusage_space = shm_toc_allocate(pcxt->toc, mul_size(sizeof(BufferUsage), pcxt->nworkers)); @@ -1411,6 +1431,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) SharedJitInstrumentation *jit_instrumentation; int instrument_options = 0; void *area_space; + char *sessionvariable_space; dsa_area *area; ParallelWorkerContext pwcxt; @@ -1436,6 +1457,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false); area = dsa_attach_in_place(area_space, seg); + /* Reconstruct session variables. */ + sessionvariable_space = shm_toc_lookup(toc, + PARALLEL_KEY_SESSION_VARIABLES, + false); + queryDesc->session_variables = + RestoreSessionVariables(&sessionvariable_space, + &queryDesc->num_session_variables); + /* Start up the executor */ queryDesc->plannedstmt->jitFlags = fpes->jit_flags; ExecutorStart(queryDesc, fpes->eflags); @@ -1504,3 +1533,117 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) FreeQueryDesc(queryDesc); receiver->rDestroy(receiver); } + +/* + * Estimate the amount of space required to serialize a session variable. + */ +static Size +EstimateSessionVariables(EState *estate) +{ + int i; + Size sz = sizeof(int); + + if (estate->es_session_variables == NULL) + return sz; + + for (i = 0; i < estate->es_num_session_variables; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + + typeOid = svarval->typid; + + sz = add_size(sz, sizeof(Oid)); /* space for type OID */ + + /* space for datum/isnull */ + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + sz = add_size(sz, + datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen)); + } + + return sz; +} + +/* + * Serialize a session variables buffer into caller-provided storage. + * + * We write the number of parameters first, as a 4-byte integer, and then + * write details for each parameter in turn. The details for each parameter + * consist of a 4-byte type OID, and then the datum as serialized by + * datumSerialize(). The caller is responsible for ensuring that there is + * enough storage to store the number of bytes that will be written; use + * EstimateSessionVariables to find out how many will be needed. + * *start_address is updated to point to the byte immediately following those + * written. + * + * RestoreSessionVariables can be used to recreate a session variable buffer + * based on the serialized representation; + */ +static void +SerializeSessionVariables(EState *estate, char **start_address) +{ + int nparams; + int i; + + /* Write number of parameters. */ + nparams = estate->es_num_session_variables; + memcpy(*start_address, &nparams, sizeof(int)); + *start_address += sizeof(int); + + /* Write each parameter in turn. */ + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + typeOid = svarval->typid; + + /* Write type OID. */ + memcpy(*start_address, &typeOid, sizeof(Oid)); + *start_address += sizeof(Oid); + + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + datumSerialize(svarval->value, svarval->isnull, typByVal, typLen, + start_address); + } +} + +static SessionVariableValue * +RestoreSessionVariables(char **start_address, int *num_session_variables) +{ + SessionVariableValue *session_variables; + int i; + int nparams; + + memcpy(&nparams, *start_address, sizeof(int)); + *start_address += sizeof(int); + + *num_session_variables = nparams; + session_variables = (SessionVariableValue *) + palloc(nparams * sizeof(SessionVariableValue)); + + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval = &session_variables[i]; + + /* Read type OID. */ + memcpy(&svarval->typid, *start_address, sizeof(Oid)); + *start_address += sizeof(Oid); + + /* Read datum/isnull. */ + svarval->value = datumRestore(start_address, &svarval->isnull); + } + + return session_variables; +} diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index a5452c0c9e4..8cee732561d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -924,25 +924,19 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) /* * We can't pass Params to workers at the moment either, so they are also - * parallel-restricted, unless they are PARAM_EXTERN Params or are - * PARAM_EXEC Params listed in safe_param_ids, meaning they could be - * either generated within workers or can be computed by the leader and - * then their value can be passed to workers. + * parallel-restricted, unless they are PARAM_EXTERN or PARAM_VARIABLE + * Params or are PARAM_EXEC Params listed in safe_param_ids, meaning they + * could be either generated within workers or can be computed by the + * leader and then their value can be passed to workers. */ else if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind == PARAM_EXTERN) + if (param->paramkind == PARAM_EXTERN || + param->paramkind == PARAM_VARIABLE) return false; - /* we don't support passing session variables to workers */ - if (param->paramkind == PARAM_VARIABLE) - { - if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) - return true; - } - if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2f1708f7c84..8df3a91c05e 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -644,12 +644,14 @@ SELECT count(*) FROM svar_test WHERE a%10 = zero; -- parallel execution is not supported yet EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; - QUERY PLAN ------------------------------------ + QUERY PLAN +-------------------------------------------- Aggregate - -> Seq Scan on svar_test - Filter: ((a % 10) = zero) -(3 rows) + -> Gather + Workers Planned: 2 + -> Parallel Seq Scan on svar_test + Filter: ((a % 10) = zero) +(5 rows) LET zero = (SELECT count(*) FROM svar_test); -- result should be 1000 -- 2.47.0 [text/x-patch] 0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch (35.6K, 11-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch) download | inline diff: From 95d56145c19788d5054a456a9ea4978b49741d32 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:05:06 +0100 Subject: [PATCH 13/21] Implementation of NOT NULL and IMMUTABLE clauses Almost trivial patch - psql, pg_dump support --- doc/src/sgml/catalogs.sgml | 20 +++ doc/src/sgml/plpgsql.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 32 +++- src/backend/catalog/pg_variable.c | 8 + src/backend/commands/session_variable.c | 69 +++++++- src/backend/parser/gram.y | 41 +++-- src/bin/pg_dump/pg_dump.c | 19 ++- src/bin/pg_dump/pg_dump.h | 2 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++ src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/pg_variable.h | 6 + src/include/nodes/parsenodes.h | 2 + src/test/regress/expected/psql.out | 36 ++-- .../regress/expected/session_variables.out | 155 +++++++++++++++++- src/test/regress/sql/session_variables.sql | 122 ++++++++++++++ 16 files changed, 523 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 124902c4e44..9a8886fc69a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9842,6 +9842,26 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <row> <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnotnull</structfield> <type>boolean</type> + </para> + <para> + True if the session variable doesn't allow null values. The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varisimmutable</structfield> <type>boolean</type> + </para> + <para> + True if the variable is <link linkend="sql-createvariable-immutable">immutable</link> (cannot be modified). + The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vareoxaction</structfield> <type>char</type> <structfield>varxactendaction</structfield> <type>char</type> </para> <para> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index bfbff9ab74e..e704e05a991 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6044,7 +6044,8 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; You can consider translating an Oracle package into a schema in <productname>PostgreSQL</productname>. Package functions and procedures would then become functions and procedures in that schema, and package - variables could be translated to session variables in that schema. + variables could be translated to session variables or immutable session + variables in that schema. (see <xref linkend="ddl-session-variables"/>). </para> </sect3> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 8187c18e28b..00938743c0d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,8 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] +CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -65,6 +65,22 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <variablelist> + <varlistentry id="sql-createvariable-immutable"> + <term><literal>IMMUTABLE</literal></term> + <listitem> + <para> + The assigned value of the session variable can not be changed. + Only if the session variable doesn't have a default value, a single + initialization is allowed using the <command>LET</command> command. Once + done, no further change is allowed until end of transaction + if the session variable was created with clause <literal>ON TRANSACTION + END RESET</literal>, or until reset of all session variables by + <command>DISCARD VARIABLES</command>, or until reset of all session + objects by command <command>DISCARD ALL</command>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-if-not-exists"> <term><literal>IF NOT EXISTS</literal></term> <listitem> @@ -105,6 +121,18 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-not-null"> + <term><literal>NOT NULL</literal></term> + <listitem> + <para> + The <literal>NOT NULL</literal> clause forbids setting the session + variable to a null value. A session variable created as NOT NULL + should not to have a declared default value, and if the variable + has not assigned value, then the reading of this variable fails. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-default"> <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> <listitem> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 36de12587c0..f261e3fd770 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -40,6 +40,8 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -55,6 +57,8 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -117,6 +121,8 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); + values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -259,6 +265,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + stmt->not_null, + stmt->is_immutable, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0a1c326e72e..7f34f93b542 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -84,6 +84,9 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool not_null; + bool is_immutable; + bool reset_at_eox; /* @@ -102,6 +105,9 @@ typedef struct SVariableData bool is_valid; uint32 hashvalue; /* used for pairing sinval message */ + + /* true, when the value is already set, and cannot be changed more */ + bool protect_value; } SVariableData; typedef SVariableData *SVariable; @@ -563,6 +569,23 @@ eval_assign_defexpr(SVariable svar, HeapTuple tup) MemoryContextSwitchTo(oldcxt); } + else + { + /* + * Raise an error if this is a NOT NULL variable but the result of + * DEFAULT expression is NULL. + */ + if (svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)), + errdetail("The result of DEFAULT expression is NULL."))); + } + + if (svar->is_immutable) + svar->protect_value = true; FreeExecutorState(estate); } @@ -593,6 +616,9 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + svar->not_null = varform->varnotnull; + svar->is_immutable = varform->varisimmutable; + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; @@ -622,9 +648,31 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); - if (!is_write) + svar->protect_value = false; + + /* + * When the variable is marked as IMMUTABLE, we prefer to evaluate + * possible DEFAULT before write op. In this case we want to protect + * default value against any overwrite. + */ + if (!is_write || + svar->is_immutable) + { eval_assign_defexpr(svar, tup); + /* + * Raise an error if this is a NOT NULL variable without default + * expression. + */ + if (svar->isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("The session variable was not initialized yet."))); + } + ReleaseSysCache(tup); } @@ -644,6 +692,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Assert(svar); Assert(!isnull || value == (Datum) 0); + if (svar->protect_value) + ereport(ERROR, + (errcode(ERRCODE_ERROR_IN_ASSIGNMENT), + errmsg("session variable \"%s.%s\" is declared IMMUTABLE", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + + /* don't allow assignment of null to NOT NULL variable */ + if (isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + /* * Use typbyval, typbylen from session variable only when they are * trustworthy (the invalidation message was not accepted for this @@ -684,6 +747,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + + /* don't allow more changes of value when variable is IMMUTABLE */ + if (svar->is_immutable) + svar->protect_value = true; } /* diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b8e33ed19db..6aea3bd35b2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,6 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <boolean> OptNotNull OptImmutable /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -5231,27 +5232,31 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $4->relpersistence = $2; - n->variable = $4; - n->typeName = $6; - n->collClause = (CollateClause *) $7; - n->defexpr = $8; - n->XactEndAction = $9; + $5->relpersistence = $2; + n->is_immutable = $3; + n->variable = $5; + n->typeName = $7; + n->collClause = (CollateClause *) $8; + n->not_null = $9; + n->defexpr = $10; + n->XactEndAction = $11; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $7->relpersistence = $2; - n->variable = $7; - n->typeName = $9; - n->collClause = (CollateClause *) $10; - n->defexpr = $11; - n->XactEndAction = $12; + $8->relpersistence = $2; + n->is_immutable = $3; + n->variable = $8; + n->typeName = $10; + n->collClause = (CollateClause *) $11; + n->not_null = $12; + n->defexpr = $13; + n->XactEndAction = $14; n->if_not_exists = true; $$ = (Node *) n; } @@ -5271,6 +5276,14 @@ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } ; +OptNotNull: NOT NULL_P { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + +OptImmutable: IMMUTABLE { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c138994304a..59f4103ca4f 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5437,6 +5437,8 @@ getVariables(Archive *fout) int i_varxactendaction; int i_varowner; int i_varcollation; + int i_varnotnull; + int i_varisimmutable; int i_varacl; int i_acldefault; int i, @@ -5459,6 +5461,8 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " v.varnotnull,\n" + " v.varisimmutable,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5479,6 +5483,8 @@ getVariables(Archive *fout) i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); + i_varnotnull = PQfnumber(res, "varnotnull"); + i_varisimmutable = PQfnumber(res, "varisimmutable"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5505,6 +5511,8 @@ getVariables(Archive *fout) pg_strdup(PQgetvalue(res, i, i_varxactendaction)); varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; + varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5551,7 +5559,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vartypname; const char *vardefexpr; const char *varxactendaction; + const char *varisimmutable; Oid varcollation; + bool varnotnull; /* skip if not to be dumped */ if (!varinfo->dobj.dump || dopt->dataOnly) @@ -5565,12 +5575,14 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; + varnotnull = varinfo->varnotnull; + varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", - qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", + varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { @@ -5582,6 +5594,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (varnotnull) + appendPQExpBuffer(query, " NOT NULL"); + if (vardefexpr) appendPQExpBuffer(query, " DEFAULT %s", vardefexpr); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 8ed83979ec9..f7921f942d0 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -717,6 +717,8 @@ typedef struct _VariableInfo char *initrvaracl; Oid varcollation; const char *rolname; /* name of owner, or empty string */ + bool varnotnull; + bool varisimmutable; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index f58ede5334a..47cb59356e0 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4041,6 +4041,42 @@ my %tests = ( }, }, + 'CREATE IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE IMMUTABLE VARIABLE test_variable NOT NULL DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dc18da350eb..cc69f07f1f9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,8 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " NOT v.varnotnull as \"%s\",\n" + " NOT v.varisimmutable as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5237,6 +5239,8 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Nullable"), + gettext_noop("Mutable"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 3fb2c4e9248..b653dda1fe8 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1273,6 +1273,8 @@ static const pgsql_thing_t words_after_create[] = { {"FOREIGN TABLE", NULL, NULL, NULL}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, + {"IMMUTABLE VARIABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE IMMUTABLE + * VARIABLE ... */ {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP}, @@ -3593,7 +3595,8 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); + COMPLETE_WITH("IMMUTABLE VARIABLE", "SEQUENCE", "TABLE", "VARIABLE", + "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3945,7 +3948,8 @@ match_previous_words(int pattern_id, /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ else if (TailMatches("CREATE", "VARIABLE", MatchAny) || - TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ @@ -4589,6 +4593,10 @@ match_previous_words(int pattern_id, else if (TailMatches("FROM", "SERVER", MatchAny, "INTO", MatchAny)) COMPLETE_WITH("OPTIONS ("); +/* IMMUTABLE -- can be expend to IMMUTABLE VARIABLE */ + else if (TailMatches("CREATE", "IMMUTABLE")) + COMPLETE_WITH("VARIABLE"); + /* INSERT --- can be inside EXPLAIN, RULE, etc */ /* Complete NOT MATCHED THEN INSERT */ else if (TailMatches("NOT", "MATCHED", "THEN", "INSERT")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index e8bfeebed11..68bc49a0e27 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,12 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* don't allow NULL */ + bool varnotnull BKI_DEFAULT(f); + + /* don't allow changes */ + bool varisimmutable BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 60404413c49..7308c323574 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,8 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + bool not_null; /* disallow nulls */ + bool is_immutable; /* don't allow changes */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index bc7b60a97ea..77a7bf45c8a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index ee153f8fb82..a8520e5579d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1617,3 +1617,148 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The session variable was not initialized yet. +--should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1; +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; +--should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DISCARD VARIABLES; +-- should to fail +LET var1 = NULL; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DROP VARIABLE var1; +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The result of DEFAULT expression is NULL. +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; +-- should to fail +LET var1 = 10; +ERROR: session variable "public.var1" is declared IMMUTABLE +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should to fail +LET var1 = 30; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 9d9d04ec76b..ac8e2cb0943 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1086,3 +1086,125 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); + +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; + +--should be ok +LET var1 = 10; +SELECT var1; + +DROP VARIABLE var1; + +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; + +--should be ok +SELECT var1; + +-- should be ok +LET var1 = 10; +SELECT var1; + +DISCARD VARIABLES; + +-- should to fail +LET var1 = NULL; + +DROP VARIABLE var1; + +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); + +-- should to fail +SELECT var1; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +SELECT var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); + +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; + +-- should be ok +SELECT var1; +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; + +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; + +-- should to fail +LET var1 = 10; +LET var1 = 20; + +-- should be ok +SELECT var1; + +-- should to fail +LET var1 = 30; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] 0012-Implementation-of-DEFAULT-clause-default-expressions.patch (33.6K, 12-0012-Implementation-of-DEFAULT-clause-default-expressions.patch) download | inline diff: From f62ba800864cd94bde0249b637b9e5485c26a16e Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:03:53 +0100 Subject: [PATCH 12/21] Implementation of DEFAULT clause - default expressions for session variables When the evaluation of default expression fails, we remove related entry from sessionvars hash table. Then sessionvars can contain only sucessfully initialized values. Then we don't need special flag for badly initialized session variables. --- doc/src/sgml/catalogs.sgml | 8 ++ doc/src/sgml/ddl.sgml | 16 ++-- doc/src/sgml/ref/create_variable.sgml | 21 +++-- doc/src/sgml/ref/discard.sgml | 3 +- src/backend/catalog/pg_variable.c | 27 ++++++ src/backend/commands/session_variable.c | 87 ++++++++++++++++++- src/backend/parser/gram.y | 15 +++- src/backend/parser/parse_agg.c | 2 + src/backend/parser/parse_expr.c | 4 + src/backend/parser/parse_func.c | 1 + src/bin/pg_dump/pg_dump.c | 14 +++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++++++ src/bin/psql/describe.c | 4 +- src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/parse_node.h | 1 + src/test/regress/expected/psql.out | 36 ++++---- .../regress/expected/session_variables.out | 73 ++++++++++++++-- src/test/regress/sql/session_variables.sql | 48 ++++++++++ 20 files changed, 355 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 02b6b27c5cb..124902c4e44 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9874,6 +9874,14 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vardefexpr</structfield> <type>pg_node_tree</type> + </para> + <para> + The internal representation of the variable default value + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 049a31a6da2..600c153ee29 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5339,14 +5339,14 @@ SELECT current_user_id; <para> The value of a session variable is local to the current session. Retrieving - a variable's value returns a <literal>NULL</literal>, unless its value has - been set to something else in the current session using the - <command>LET</command> command. Session variables are not transactional: - any changes made to the value of a session variable in a transaction won't - be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves can be persistent - or temporary, but their values are neither persistent nor shared (like the - content of temporary tables). + a variable's value returns a <literal>NULL</literal> or a default value, + unless its value has been set to something else in the current session + using the <command>LET</command> command. Session variables are not + transactional: any changes made to the value of a session variable in + a transaction won't be undone if the transaction is rolled back (just like + variables in procedural languages). Session variables themselves can be + persistent or temporary, but their values are neither persistent nor shared + (like the content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index e1c2940d4ca..8187c18e28b 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] + [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -41,10 +41,10 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <para> The value of a session variable is local to the current session. Retrieving - a session variable's value returns NULL, unless its value is set to - something else in the current session with a <command>LET</command> command. - The content of a session variable is not transactional. This is the same as - regular variables in procedural languages. + a session variable's value returns NULL or a default value, unless its value + is set to something else in the current session with a <command>LET</command> + command. The content of a session variable is not transactional. This is the + same as regular variables in procedural languages. </para> <para> @@ -105,6 +105,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-default"> + <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> + <listitem> + <para> + The <literal>DEFAULT</literal> clause can be used to assign a default + value to a session variable. This expression is evaluated when the session + variable is first accessed for reading and had not yet been assigned a value. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> <term><literal>ON COMMIT DROP</literal></term> <listitem> diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index 61b967f9c9b..6b0cb95034f 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -71,7 +71,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } <listitem> <para> Resets the value of all session variables. If a variable - is later reused, it is re-initialized to <literal>NULL</literal>. + is later reused, it is re-initialized to either + <literal>NULL</literal> or its default value. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 4520eed6da8..36de12587c0 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -24,6 +24,9 @@ #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "miscadmin.h" +#include "parser/parse_coerce.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -37,6 +40,7 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -51,6 +55,7 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction) { Acl *varacl; @@ -114,6 +119,11 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); + if (varDefexpr) + values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr)); + else + nulls[Anum_pg_variable_vardefexpr - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); if (varacl != NULL) @@ -150,6 +160,11 @@ create_variable(const char *varName, record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); free_object_addresses(addrs); + /* dependency on default expr */ + if (varDefexpr) + recordDependencyOnExpr(&myself, (Node *) varDefexpr, + NIL, DEPENDENCY_NORMAL); + /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); @@ -185,6 +200,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid collation; Oid typcollation; ObjectAddress variable; + Node *cooked_default = NULL; /* check consistency of arguments */ if (stmt->XactEndAction == VARIABLE_XACTEND_DROP @@ -226,6 +242,16 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) format_type_be(typid)), parser_errposition(pstate, stmt->collClause->location))); + if (stmt->defexpr) + { + cooked_default = transformExpr(pstate, stmt->defexpr, + EXPR_KIND_VARIABLE_DEFAULT); + + cooked_default = coerce_to_specific_type(pstate, + cooked_default, typid, "DEFAULT"); + assign_expr_collations(pstate, cooked_default); + } + variable = create_variable(stmt->variable->relname, namespaceid, typid, @@ -233,6 +259,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + cooked_default, stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0b512815d4a..0a1c326e72e 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/svariableReceiver.h" #include "funcapi.h" #include "miscadmin.h" +#include "optimizer/optimizer.h" #include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" @@ -510,11 +511,68 @@ AtEOSubXact_SessionVariables(bool isCommit, } } +/* + * evaluate an expression + */ +static void +eval_assign_defexpr(SVariable svar, HeapTuple tup) +{ + Datum defexpr_value; + bool isnull; + + Assert(svar); + Assert(svar->is_valid); + Assert(HeapTupleIsValid(tup)); + + defexpr_value = SysCacheGetAttr(VARIABLEOID, + tup, + Anum_pg_variable_vardefexpr, + &isnull); + + if (!isnull) + { + EState *estate; + ExprState *defexprs; + Expr *defexpr; + char *defexpr_str; + Datum value; + MemoryContext oldcxt; + + estate = CreateExecutorState(); + + defexpr_str = TextDatumGetCString(defexpr_value); + defexpr = (Expr *) stringToNode(defexpr_str); + + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + + defexpr = expression_planner((Expr *) defexpr); + defexprs = ExecInitExpr(defexpr, NULL); + + value = ExecEvalExprSwitchContext(defexprs, + GetPerTupleExprContext(estate), + &isnull); + + MemoryContextSwitchTo(oldcxt); + + if (!isnull) + { + oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + svar->value = datumCopy(value, svar->typbyval, svar->typlen); + svar->isnull = false; + + MemoryContextSwitchTo(oldcxt); + } + + FreeExecutorState(estate); + } +} + /* * Initialize attributes cached in "svar" */ static void -setup_session_variable(SVariable svar, Oid varid) +setup_session_variable(SVariable svar, Oid varid, bool is_write) { HeapTuple tup; Form_pg_variable varform; @@ -564,6 +622,9 @@ setup_session_variable(SVariable svar, Oid varid) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!is_write) + eval_assign_defexpr(svar, tup); + ReleaseSysCache(tup); } @@ -593,7 +654,7 @@ set_session_variable(SVariable svar, Datum value, bool isnull) */ if (!svar->is_valid) { - setup_session_variable(&locsvar, svar->varid); + setup_session_variable(&locsvar, svar->varid, false); _svar = &locsvar; } else @@ -726,7 +787,25 @@ get_session_variable(Oid varid) */ if (!svar->is_valid) { - setup_session_variable(svar, varid); + /* in this case we want to use defexp if it is defined */ + PG_TRY(); + { + /* + * In this case, the setup can execute default expression. When + * the execution of default expression fails, then we need to + * remove entry from session vars. + */ + setup_session_variable(svar, varid, false); + } + PG_CATCH(); + { + /* this entry cannot be valid, remove from sessionvars */ + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + + /* propagate the error */ + PG_RE_THROW(); + } + PG_END_TRY(); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", get_namespace_name(get_session_variable_namespace(varid)), @@ -790,7 +869,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!found) { - setup_session_variable(svar, varid); + setup_session_variable(svar, varid, true); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", get_namespace_name(get_session_variable_namespace(svar->varid)), diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d8de8acf944..b8e33ed19db 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -638,6 +638,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <partboundspec> PartitionBoundSpec %type <list> hash_partbound %type <defelt> hash_partbound_elem +%type <node> OptSessionVarDefExpr %type <node> json_format_clause json_format_clause_opt @@ -5230,30 +5231,36 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $4->relpersistence = $2; n->variable = $4; n->typeName = $6; n->collClause = (CollateClause *) $7; - n->XactEndAction = $8; + n->defexpr = $8; + n->XactEndAction = $9; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $7->relpersistence = $2; n->variable = $7; n->typeName = $9; n->collClause = (CollateClause *) $10; - n->XactEndAction = $11; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = true; $$ = (Node *) n; } ; +OptSessionVarDefExpr: DEFAULT b_expr { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + /* * Temporary session variables can be dropped on successful * transaction end like tables. diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index a7343001f21..ddfbcf42b74 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -488,6 +488,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: if (isAgg) err = _("aggregate functions are not allowed in DEFAULT expressions"); @@ -937,6 +938,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("window functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 3602c3572d7..bcbb1f564af 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -582,6 +582,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind) case EXPR_KIND_JOIN_USING: case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: result = false; break; } @@ -667,6 +668,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: case EXPR_KIND_LET_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: /* okay */ break; @@ -2075,6 +2077,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("cannot use subquery in DEFAULT expression"); break; case EXPR_KIND_INDEX_EXPRESSION: @@ -3427,6 +3430,7 @@ ParseExprKindName(ParseExprKind exprKind) return "CHECK"; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: return "DEFAULT"; case EXPR_KIND_INDEX_EXPRESSION: return "index expression"; diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9aa4f60768b..fcac694455d 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2620,6 +2620,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("set-returning functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 2ec6a22a266..c138994304a 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_vardefexpr; int i_varxactendaction; int i_varowner; int i_varcollation; @@ -5458,6 +5459,7 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" "FROM pg_catalog.pg_variable v\n" @@ -5474,6 +5476,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); @@ -5509,6 +5512,11 @@ getVariables(Archive *fout) varinfo[i].dacl.initprivs = NULL; varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + if (PQgetisnull(res, i, i_vardefexpr)) + varinfo[i].vardefexpr = NULL; + else + varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr)); + /* do not try to dump ACL if no ACL exists */ if (!PQgetisnull(res, i, i_varacl)) varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; @@ -5541,6 +5549,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *vardefexpr; const char *varxactendaction; Oid varcollation; @@ -5553,6 +5562,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; @@ -5572,6 +5582,10 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (vardefexpr) + appendPQExpBuffer(query, " DEFAULT %s", + vardefexpr); + if (strcmp(varxactendaction, "r") == 0) appendPQExpBuffer(query, " ON TRANSACTION END RESET"); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7c0c03749e8..8ed83979ec9 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *vardefexpr; char *varxactendaction; char *varacl; char *rvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 419ea39727b..f58ede5334a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4005,6 +4005,42 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE VARIABLE test_variable DEFAULT ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d0f69f8c8e9..dc18da350eb 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,7 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" @@ -5236,6 +5237,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Default"), gettext_noop("Transactional end action")); if (verbose) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 60291ef7ac2..e8bfeebed11 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -67,6 +67,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* access permissions */ aclitem varacl[1] BKI_DEFAULT(_null_); + /* list of expression trees for variable default (NULL if none) */ + pg_node_tree vardefexpr BKI_DEFAULT(_null_); + #endif } FormData_pg_variable; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7a519e08429..60404413c49 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index c11fdb3d970..fa566b194d1 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -84,6 +84,7 @@ typedef enum ParseExprKind EXPR_KIND_CYCLE_MARK, /* cycle mark value */ EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ EXPR_KIND_LET_TARGET, /* LET target */ + EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */ } ParseExprKind; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 61077a52b16..bc7b60a97ea 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 99e4ac72be1..ee153f8fb82 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1554,3 +1554,66 @@ SELECT var1 IS NULL; (1 row) DROP VARIABLE var1; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); +ERROR: cannot drop function vartest_fx() because other objects depend on it +DETAIL: session variable var1 depends on function vartest_fx() +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +SELECT var1; +NOTICE: vartest_fx executed + var1 +------ + 0 +(1 row) + +-- the defexpr should be evaluated only once +SELECT var1; + var1 +------ + 0 +(1 row) + +DISCARD VARIABLES; +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DISCARD VARIABLES; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +-- should to fail, but not to crash +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- again +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- but we can write +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 7853261080b..9d9d04ec76b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1038,3 +1038,51 @@ ROLLBACK; SELECT var1 IS NULL; DROP VARIABLE var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); + +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); + +-- should be ok +SELECT var1; + +-- the defexpr should be evaluated only once +SELECT var1; + +DISCARD VARIABLES; + +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + +DISCARD VARIABLES; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +-- should to fail, but not to crash +SELECT var1; + +-- again +SELECT var1; + +-- but we can write +LET var1 = 100; +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); -- 2.47.0 [text/x-patch] 0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch (14.6K, 13-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch) download | inline diff: From 1428307b22c5d9c379a84dfa5f17cd3bffdaf0f2 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:02:13 +0100 Subject: [PATCH 11/21] Implementation ON TRANSACTION END RESET clause This is simple patch - just add special flag to session variable memory entry. The entries with active this flag are removed from sessionvars hash table at transaction end. The "TRANSACTION END" is synonyms for "COMMIT ROLLBACK" but the "TRANSACTION END" is more illustrative and less confusing then "COMMIT ROLLBACK". --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 13 ++++- src/backend/commands/session_variable.c | 51 +++++++++++++++++++ src/backend/parser/gram.y | 1 + src/bin/pg_dump/pg_dump.c | 11 ++++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 18 +++++++ src/bin/psql/describe.c | 1 + src/include/catalog/pg_variable.h | 1 + .../isolation/expected/session-variable.out | 4 +- .../isolation/specs/session-variable.spec | 4 +- .../regress/expected/session_variables.out | 34 +++++++++++++ src/test/regress/sql/session_variables.sql | 20 ++++++++ 13 files changed, 158 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 94db502b72e..02b6b27c5cb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9846,7 +9846,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para> <para> Action performed at end of transaction: - <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + <literal>n</literal> = no action, <literal>d</literal> = drop the variable, + <literal>r</literal> = reset the variable to its default value. </para></entry> </row> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 642346186f0..e1c2940d4ca 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ ON COMMIT DROP ] + [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -117,6 +117,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-transaction-end-reset"> + <term><literal>ON TRANSACTION END RESET</literal></term> + <listitem> + <para> + The <literal>ON TRANSACTION END RESET</literal> clause causes the session + variable to be reset to its default value when the transaction is committed + or rolled back. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index b23ae21ba55..0b512815d4a 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -83,6 +83,8 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool reset_at_eox; + /* * Top level local transaction id of the last transaction that dropped the * variable, if any. We need this information to avoid freeing memory for @@ -110,6 +112,12 @@ static MemoryContext SVariableMemoryContext = NULL; /* becomes true when we receive a sinval message */ static bool needs_validation = false; +/* + * true, when some used session variable has ON COMMIT DROP + * or ON TRANSACTION END RESET clauses + */ +static bool has_session_variables_with_reset_at_eox = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -385,6 +393,32 @@ remove_invalid_session_variables(bool atEOX) } } +/* + * remove entries marked as "reset_at_eox" + */ +static void +remove_session_variables_with_reset_at_eox(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_session_variables_with_reset_at_eox) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->reset_at_eox) + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + } + + has_session_variables_with_reset_at_eox = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -392,6 +426,8 @@ remove_invalid_session_variables(bool atEOX) void AtPreEOXact_SessionVariables(bool isCommit) { + remove_session_variables_with_reset_at_eox(); + if (isCommit) { if (xact_drop_items) @@ -503,6 +539,21 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + /* + * We don't need to explicitly reset variables marked ON COMMIT DROP. It + * can be done by sinval message processing. But this processing can be + * postponed due aborted transaction. On second hand there is not a + * reason, why don't do it at transaction end immediately. + */ + if (varform->varxactendaction == VARIABLE_XACTEND_RESET || + varform->varxactendaction == VARIABLE_XACTEND_DROP) + { + svar->reset_at_eox = true; + has_session_variables_with_reset_at_eox = true; + } + else + svar->reset_at_eox = false; + svar->drop_lxid = InvalidTransactionId; svar->isnull = true; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 76655aa8aeb..d8de8acf944 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5259,6 +5259,7 @@ CreateSessionVarStmt: * transaction end like tables. */ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | ON TRANSACTION END_P RESET { $$ = VARIABLE_XACTEND_RESET; } | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } ; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b8d839ece41..2ec6a22a266 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_varxactendaction; int i_varowner; int i_varcollation; int i_varacl; @@ -5450,6 +5451,7 @@ getVariables(Archive *fout) /* get the variables in current database */ appendPQExpBuffer(query, "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varxactendaction,\n" " v.varnamespace, v.vartype,\n" " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" " CASE WHEN v.varcollation <> t.typcollation " @@ -5472,6 +5474,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); i_varowner = PQfnumber(res, "varowner"); @@ -5495,6 +5498,9 @@ getVariables(Archive *fout) varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varxactendaction = + pg_strdup(PQgetvalue(res, i, i_varxactendaction)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); @@ -5535,6 +5541,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *varxactendaction; Oid varcollation; /* skip if not to be dumped */ @@ -5546,6 +5553,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", @@ -5564,6 +5572,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (strcmp(varxactendaction, "r") == 0) + appendPQExpBuffer(query, " ON TRANSACTION END RESET"); + appendPQExpBuffer(query, ";\n"); if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f2f558b2961..7c0c03749e8 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *varxactendaction; char *varacl; char *rvaracl; char *initvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index dd8c054a6a6..419ea39727b 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3987,6 +3987,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d02f6416f0e..d0f69f8c8e9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 1e6dd98d415..60291ef7ac2 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -74,6 +74,7 @@ typedef enum VariableXactEndAction { VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ + VARIABLE_XACTEND_RESET = 'r', /* ON TRANSACTION END RESET */ } VariableXactEndAction; /* ---------------- diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index a609797dc5d..e9a254bcd12 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,11 +86,12 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; +step o_eox_r: CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; @@ -103,6 +104,7 @@ t step discard: DISCARD VARIABLES; step sc3: COMMIT; +step clean: DROP VARIABLE myvar_o_eox_r; step state: SELECT varname FROM pg_variable; varname ------- diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index 45e65d4085d..5d089c8a4ed 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -25,12 +25,14 @@ session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } +step o_eox_r { CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } step discard { DISCARD VARIABLES; } step sc3 { COMMIT; } +step clean { DROP VARIABLE myvar_o_eox_r; } step state { SELECT varname FROM pg_variable; } session s4 @@ -48,4 +50,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 6dadb9074da..99e4ac72be1 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1520,3 +1520,37 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be NULL; +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be NULL +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 770ce650685..7853261080b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1018,3 +1018,23 @@ COMMIT; SELECT count(*) FROM pg_variable WHERE varname = 'var1'; -- should be zero SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; + +BEGIN; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be NULL; +SELECT var1 IS NULL; + +BEGIN; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be NULL +SELECT var1 IS NULL; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] 0010-implementation-of-temporary-session-variables.patch (42.2K, 14-0010-implementation-of-temporary-session-variables.patch) download | inline diff: From 005996b9e9a828709ea525ef4a199a3905296e2f Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:00:28 +0100 Subject: [PATCH 10/21] implementation of temporary session variables The temporary variables are created inside temp schema and they are dropped by dropping this schema. For consistency with temp tables, the CREATE TEMP VARIABLE command supports ON COMMIT DROP clause. The temporary variables with this clause are collected in xact_drop_items list. This list should be carefully maintained and can contain only valid entries (not yet dropped). From this reasons now the subcommit and subaborting have to be handled. --- doc/src/sgml/catalogs.sgml | 10 + doc/src/sgml/ddl.sgml | 6 +- doc/src/sgml/ref/create_variable.sgml | 15 +- src/backend/access/transam/xact.c | 9 + src/backend/catalog/pg_variable.c | 24 +- src/backend/commands/session_variable.c | 218 +++++++++++++++++- src/backend/commands/view.c | 2 +- src/backend/parser/analyze.c | 4 +- src/backend/parser/gram.y | 30 ++- src/backend/parser/parse_relation.c | 22 +- src/bin/psql/describe.c | 10 +- src/bin/psql/tab-complete.in.c | 5 +- src/include/catalog/pg_variable.h | 9 + src/include/commands/session_variable.h | 6 +- src/include/nodes/parsenodes.h | 1 + src/include/nodes/primnodes.h | 4 +- src/include/parser/parse_relation.h | 2 +- .../isolation/expected/session-variable.out | 3 +- .../isolation/specs/session-variable.spec | 3 +- src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 130 ++++++++++- src/test/regress/sql/session_variables.sql | 66 ++++++ src/tools/pgindent/typedefs.list | 1 + 23 files changed, 546 insertions(+), 70 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 0c15d56dd42..94db502b72e 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9840,6 +9840,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varxactendaction</structfield> <type>char</type> + </para> + <para> + Action performed at end of transaction: + <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varcollation</structfield> <type>oid</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index ce1b1e1f599..049a31a6da2 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5344,9 +5344,9 @@ SELECT current_user_id; <command>LET</command> command. Session variables are not transactional: any changes made to the value of a session variable in a transaction won't be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves are persistent, but - their values are neither persistent nor shared (like the content of - temporary tables). + procedural languages). Session variables themselves can be persistent + or temporary, but their values are neither persistent nor shared (like the + content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 05bf67e3411..642346186f0 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ ON COMMIT DROP ] </synopsis> </refsynopsisdiv> <refsect1> @@ -104,6 +105,18 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> + <term><literal>ON COMMIT DROP</literal></term> + <listitem> + <para> + The <literal>ON COMMIT DROP</literal> clause specifies the behaviour of a + temporary session variable at transaction commit. With this clause, the + session variable is dropped at commit time. The clause is only allowed + for temporary variables. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b7ebcc2a557..01e3d469306 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -36,6 +36,7 @@ #include "catalog/pg_enum.h" #include "catalog/storage.h" #include "commands/async.h" +#include "commands/session_variable.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "common/pg_prng.h" @@ -2316,6 +2317,9 @@ CommitTransaction(void) */ smgrDoPendingSyncs(true, is_parallel_worker); + /* remove values of dropped session variables from memory */ + AtPreEOXact_SessionVariables(true); + /* close large objects before lower-level cleanup */ AtEOXact_LargeObject(true); @@ -2912,6 +2916,7 @@ AbortTransaction(void) AtAbort_Portals(); smgrDoPendingSyncs(false, is_parallel_worker); AtEOXact_LargeObject(false); + AtPreEOXact_SessionVariables(false); AtAbort_Notify(); AtEOXact_RelationMap(false, is_parallel_worker); AtAbort_Twophase(); @@ -5190,6 +5195,8 @@ CommitSubTransaction(void) AtEOSubXact_SPI(true, s->subTransactionId); AtEOSubXact_on_commit_actions(true, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(true, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(true, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(true, s->subTransactionId, @@ -5355,6 +5362,8 @@ AbortSubTransaction(void) AtEOSubXact_SPI(false, s->subTransactionId); AtEOSubXact_on_commit_actions(false, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(false, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(false, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(false, s->subTransactionId, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index c7369e2b2ac..4520eed6da8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -36,7 +36,8 @@ static ObjectAddress create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists); + bool if_not_exists, + VariableXactEndAction varXactEndAction); /* @@ -49,7 +50,8 @@ create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists) + bool if_not_exists, + VariableXactEndAction varXactEndAction) { Acl *varacl; NameData varname; @@ -110,6 +112,7 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); @@ -183,6 +186,13 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid typcollation; ObjectAddress variable; + /* check consistency of arguments */ + if (stmt->XactEndAction == VARIABLE_XACTEND_DROP + && stmt->variable->relpersistence != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("ON COMMIT DROP can only be used on temporary variables"))); + namespaceid = RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); @@ -222,7 +232,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) typmod, varowner, collation, - stmt->if_not_exists); + stmt->if_not_exists, + stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", stmt->variable->relname, variable.objectId); @@ -230,6 +241,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) /* we want SessionVariableCreatePostprocess to see the catalog changes. */ CommandCounterIncrement(); + SessionVariableCreatePostprocess(variable.objectId, stmt->XactEndAction); + return variable; } @@ -242,6 +255,7 @@ DropVariableById(Oid varid) { Relation rel; HeapTuple tup; + char XactEndAction; rel = table_open(VariableRelationId, RowExclusiveLock); @@ -250,6 +264,8 @@ DropVariableById(Oid varid) if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for variable %u", varid); + XactEndAction = ((Form_pg_variable) GETSTRUCT(tup))->varxactendaction; + CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); @@ -257,5 +273,5 @@ DropVariableById(Oid varid) table_close(rel, RowExclusiveLock); /* do the necessary cleanup in local memory, if needed */ - SessionVariableDropPostprocess(varid); + SessionVariableDropPostprocess(varid, XactEndAction); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 21c7a9517b5..b23ae21ba55 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -16,6 +16,8 @@ #include "access/xact.h" #include "catalog/pg_variable.h" +#include "catalog/dependency.h" +#include "catalog/namespace.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" #include "funcapi.h" @@ -31,6 +33,19 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" +typedef struct SVariableXActDropItem +{ + Oid varid; /* varid of session variable */ + + /* + * creating_subid is the ID of the creating subxact. If the action was + * unregistered during the current transaction, deleting_subid is the ID + * of the deleting subxact, otherwise InvalidSubTransactionId. + */ + SubTransactionId creating_subid; + SubTransactionId deleting_subid; +} SVariableXActDropItem; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -96,13 +111,21 @@ static MemoryContext SVariableMemoryContext = NULL; static bool needs_validation = false; /* - * The content of dropped session variables is not removed immediately. We do - * that in the next transaction that reads or writes a session variable. - * "validated_lxid" stores the transaction that performed said validation, so - * that we can avoid repeating the effort. + * The content of dropped session variables is not removed immediately. If + * possible, we do that at the end of the transaction. But we cannot do that + * if the transaction aborted, because we lost access to the system catalog. + * In that case, we clean up in the next transaction that reads or writes a + * session variable. "validated_lxid" stores the transaction that performed + * said validation, so that we can avoid repeating the effort. */ static LocalTransactionId validated_lxid = InvalidLocalTransactionId; +/* list holds fields of SVariableXActDropItem type */ +static List *xact_drop_items = NIL; + +static void register_session_variable_xact_drop(Oid varid); +static void unregister_session_variable_xact_drop(Oid varid); + /* * Callback function for session variable invalidation. */ @@ -139,16 +162,45 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) } } +/* + * Do the necessary work to setup local memory management of a new + * variable. + * + * Caller should already have created the necessary entry in catalog + * and made them visible. + */ +void +SessionVariableCreatePostprocess(Oid varid, char XactEndAction) +{ + /* + * For temporary variables, we need to create a new end of xact action to + * ensure deletion from catalog. + */ + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + register_session_variable_xact_drop(varid); + } +} + /* * Handle the local memory cleanup for a DROP VARIABLE command. * * Caller should take care of removing the pg_variable entry first. */ void -SessionVariableDropPostprocess(Oid varid) +SessionVariableDropPostprocess(Oid varid, char XactEndAction) { Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + unregister_session_variable_xact_drop(varid); + } + if (sessionvars) { bool found; @@ -170,6 +222,57 @@ SessionVariableDropPostprocess(Oid varid) } } +/* + * Registration of actions to be executed on session variables at transaction + * end time. We want to drop temporary session variables with clause ON COMMIT + * DROP. + */ + +/* + * Register a session variable xact action. + */ +static void +register_session_variable_xact_drop(Oid varid) +{ + SVariableXActDropItem *xact_ai; + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + + xact_ai = (SVariableXActDropItem *) + palloc(sizeof(SVariableXActDropItem)); + + xact_ai->varid = varid; + + xact_ai->creating_subid = GetCurrentSubTransactionId(); + xact_ai->deleting_subid = InvalidSubTransactionId; + + xact_drop_items = lcons(xact_ai, xact_drop_items); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Unregister an id of a given session variable from drop list. In this + * moment, the action is just marked as deleted by setting deleting_subid. The + * calling even might be rollbacked, in which case we should not lose this + * action. + */ +static void +unregister_session_variable_xact_drop(Oid varid) +{ + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + if (xact_ai->varid == varid) + xact_ai->deleting_subid = GetCurrentSubTransactionId(); + } +} + /* * Release stored value, free memory */ @@ -222,9 +325,11 @@ is_session_variable_valid(SVariable svar) /* * Check all potentially invalid session variable data in local memory and free * the memory for all invalid ones. This function is called before any read or - * write of a session variable. Freeing of a variable's memory is postponed if - * the variable has been dropped by the current transaction, since that - * operation could still be rolled back. + * write of a session variable or when the transaction ends. At the end of a + * transaction (atEOX is true) we can discard all invalid variables. Inside a + * transaction (atEOX is false) we postpone freeing a variable's memory if the + * variable has been dropped by the current transaction, since that operation + * could still be rolled back. * * It is possible that we receive a cache invalidation message while * remove_invalid_session_variables() is executing, so we cannot guarantee that @@ -232,7 +337,7 @@ is_session_variable_valid(SVariable svar) * done. However, we can guarantee that all entries get checked once. */ static void -remove_invalid_session_variables(void) +remove_invalid_session_variables(bool atEOX) { HASH_SEQ_STATUS status; SVariable svar; @@ -259,7 +364,7 @@ remove_invalid_session_variables(void) { if (!svar->is_valid) { - if (svar->drop_lxid == MyProc->vxid.lxid) + if (!atEOX && svar->drop_lxid == MyProc->vxid.lxid) { /* try again in the next transaction */ needs_validation = true; @@ -280,6 +385,95 @@ remove_invalid_session_variables(void) } } +/* + * Perform ON COMMIT DROP for temporary session variables, + * and remove all dropped variables from memory. + */ +void +AtPreEOXact_SessionVariables(bool isCommit) +{ + if (isCommit) + { + if (xact_drop_items) + { + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + /* iterate only over entries that are still pending */ + if (xact_ai->deleting_subid == InvalidSubTransactionId) + { + ObjectAddress object; + + object.classId = VariableRelationId; + object.objectId = xact_ai->varid; + object.objectSubId = 0; + + /* + * Since this is an automatic drop, rather than one + * directly initiated by the user, we pass the + * PERFORM_DELETION_INTERNAL flag. + */ + elog(DEBUG1, "session variable (oid:%u) will be deleted (forced by ON COMMIT DROP clause)", + xact_ai->varid); + + performDeletion(&object, DROP_CASCADE, + PERFORM_DELETION_INTERNAL | + PERFORM_DELETION_QUIETLY); + } + } + } + + remove_invalid_session_variables(true); + } + + /* + * We have to clean xact_drop_items. All related variables are dropped + * now, or lost inside aborted transaction. + */ + list_free_deep(xact_drop_items); + xact_drop_items = NULL; +} + +/* + * Post-subcommit or post-subabort cleanup of xact drop list. + * + * During subabort, we can immediately remove entries created during this + * subtransaction. During subcommit, just transfer entries marked during + * this subtransaction as being the parent's responsibility. + */ +void +AtEOSubXact_SessionVariables(bool isCommit, + SubTransactionId mySubid, + SubTransactionId parentSubid) +{ + ListCell *cur_item; + + foreach(cur_item, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(cur_item); + + if (!isCommit && xact_ai->creating_subid == mySubid) + { + /* cur_item must be removed */ + xact_drop_items = foreach_delete_current(xact_drop_items, cur_item); + pfree(xact_ai); + } + else + { + /* cur_item must be preserved */ + if (xact_ai->creating_subid == mySubid) + xact_ai->creating_subid = parentSubid; + if (xact_ai->deleting_subid == mySubid) + xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -438,7 +632,7 @@ get_session_variable(Oid varid) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; @@ -534,7 +728,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 2bd49eb55e6..c736c5c46f3 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -484,7 +484,7 @@ DefineView(ViewStmt *stmt, const char *queryString, */ view = copyObject(stmt->view); /* don't corrupt original command */ if (view->relpersistence == RELPERSISTENCE_PERMANENT - && isQueryUsingTempRelation(viewParse)) + && isQueryUsingTempObject(viewParse)) { view->relpersistence = RELPERSISTENCE_TEMP; ereport(NOTICE, diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 81b8b0a633d..9dfd435128a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3390,10 +3390,10 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) * creation query. It would be hard to refresh data or incrementally * maintain it if a source disappeared. */ - if (isQueryUsingTempRelation(query)) + if (isQueryUsingTempObject(query)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("materialized views must not use temporary tables or views"))); + errmsg("materialized views must not use temporary tables, views or session variables"))); /* * A materialized view would either need to save parameters for use in diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af511d12aa1..76655aa8aeb 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -466,6 +466,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <ival> OptTemp %type <ival> OptNoLog %type <oncommit> OnCommitOption +%type <ival> XactEndActionOption %type <ival> for_locking_strength %type <node> for_locking_item @@ -5229,26 +5230,39 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $3; - n->typeName = $5; - n->collClause = (CollateClause *) $6; + $4->relpersistence = $2; + n->variable = $4; + n->typeName = $6; + n->collClause = (CollateClause *) $7; + n->XactEndAction = $8; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $6; - n->typeName = $8; - n->collClause = (CollateClause *) $9; + $7->relpersistence = $2; + n->variable = $7; + n->typeName = $9; + n->collClause = (CollateClause *) $10; + n->XactEndAction = $11; n->if_not_exists = true; $$ = (Node *) n; } ; +/* + * Temporary session variables can be dropped on successful + * transaction end like tables. + */ +XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } + ; + + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 8075b1b8a1b..95ae2108044 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -101,7 +101,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); -static bool isQueryUsingTempRelation_walker(Node *node, void *context); +static bool isQueryUsingTempObject_walker(Node *node, void *context); /* @@ -3896,13 +3896,13 @@ rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte) * the query is a temporary relation (table, view, or materialized view). */ bool -isQueryUsingTempRelation(Query *query) +isQueryUsingTempObject(Query *query) { - return isQueryUsingTempRelation_walker((Node *) query, NULL); + return isQueryUsingTempObject_walker((Node *) query, NULL); } static bool -isQueryUsingTempRelation_walker(Node *node, void *context) +isQueryUsingTempObject_walker(Node *node, void *context) { if (node == NULL) return false; @@ -3928,13 +3928,23 @@ isQueryUsingTempRelation_walker(Node *node, void *context) } return query_tree_walker(query, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context, QTW_IGNORE_JOINALIASES); } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (isAnyTempNamespace(get_session_variable_namespace(p->paramvarid))) + return true; + } + } return expression_tree_walker(node, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context); } diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index c52c8fd147f..d02f6416f0e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5226,12 +5226,16 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" - " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " CASE v.varxactendaction\n" + " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), gettext_noop("Type"), gettext_noop("Collation"), - gettext_noop("Owner")); + gettext_noop("Owner"), + gettext_noop("Transactional end action")); if (verbose) { diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index df3e2539270..3fb2c4e9248 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3593,7 +3593,7 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW"); + COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3944,7 +3944,8 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ - else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + else if (TailMatches("CREATE", "VARIABLE", MatchAny) || + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index db593cd5bed..1e6dd98d415 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* action on transaction end */ + char varxactendaction BKI_DEFAULT(n); + /* variable collation */ Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); @@ -67,6 +70,12 @@ CATALOG(pg_variable,9222,VariableRelationId) #endif } FormData_pg_variable; +typedef enum VariableXactEndAction +{ + VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ + VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ +} VariableXactEndAction; + /* ---------------- * Form_pg_variable corresponds to a pointer to a tuple with * the format of the pg_variable relation. diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 3dab8ae2a48..dc83296b1ba 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,7 +21,11 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" -extern void SessionVariableDropPostprocess(Oid varid); +extern void SessionVariableCreatePostprocess(Oid varid, char XactEndAction); +extern void SessionVariableDropPostprocess(Oid varid, char XactEndAction); +extern void AtPreEOXact_SessionVariables(bool isCommit); +extern void AtEOSubXact_SessionVariables(bool isCommit, SubTransactionId mySubid, + SubTransactionId parentSubid); extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 04ab22bd492..7a519e08429 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 4b2424d6d34..6174a5562c5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -51,7 +51,9 @@ typedef struct Alias List *colnames; /* optional list of column aliases */ } Alias; -/* What to do at commit time for temporary relations */ +/* + * What to do at commit time for temporary relations or session variables. + */ typedef enum OnCommitAction { ONCOMMIT_NOOP, /* No ON COMMIT clause (do nothing) */ diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index 91fd8e243b5..a8117bcae3f 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -126,6 +126,6 @@ extern int attnameAttNum(Relation rd, const char *attname, bool sysColOK); extern const NameData *attnumAttName(Relation rd, int attid); extern Oid attnumTypeId(Relation rd, int attid); extern Oid attnumCollationId(Relation rd, int attid); -extern bool isQueryUsingTempRelation(Query *query); +extern bool isQueryUsingTempObject(Query *query); #endif /* PARSE_RELATION_H */ diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index 0a5579dc7ce..a609797dc5d 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,10 +86,11 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; +step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index c864fee4006..45e65d4085d 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -24,6 +24,7 @@ step create { CREATE VARIABLE myvar AS text; } session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } +step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } @@ -47,4 +48,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 88e21194711..61077a52b16 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index cc4b5964799..6dadb9074da 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| - | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1400,3 +1400,123 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; +NOTICE: view "var_test_view" will be a temporary view +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view var_test_view +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 65c2a79425d..770ce650685 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -826,6 +826,7 @@ BEGIN; DROP VARIABLE var1; SELECT var2; ROLLBACK; + -- should be ok SELECT var1; @@ -952,3 +953,68 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; + +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; + +DROP VARIABLE var1 CASCADE; + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 68e3c3c7b63..64ddf589b3c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2798,6 +2798,7 @@ SupportRequestWFuncMonotonic SVariable SVariableData SVariableState +SVariableXActDropItem Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] 0009-PREPARE-LET-support.patch (7.4K, 15-0009-PREPARE-LET-support.patch) download | inline diff: From 55b3107be6fef19f9387599ef9ec589bdb74a44e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 09:44:58 +0100 Subject: [PATCH 09/21] 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.in.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 <replaceable class="parameter">name</replaceable> [ ( <replaceable class <listitem> <para> Any <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, - <command>DELETE</command>, <command>MERGE</command>, or <command>VALUES</command> - statement. + <command>DELETE</command>, <command>MERGE</command>, <command>VALUES</command>, + or <command>LET</command> statement. </para> </listitem> </varlistentry> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index def7dde2330..af511d12aa1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12161,7 +12161,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.in.c b/src/bin/psql/tab-complete.in.c index 3ef4776d98a..df3e2539270 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4629,9 +4629,9 @@ match_previous_words(int pattern_id, 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 <variable> 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 a867fdd3e75..cc4b5964799 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -439,9 +439,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 @@ -1321,5 +1321,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 8a6dbffd54e..65c2a79425d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -893,5 +893,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.47.0 [text/x-patch] 0008-EXPLAIN-LET-support.patch (8.2K, 16-0008-EXPLAIN-LET-support.patch) download | inline diff: From c5f9c60c3b5f4d39290226f478ca15aad747e20e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 18:28:07 +0200 Subject: [PATCH 08/21] EXPLAIN LET support Enhancing ExplainOnePlan is necessary to be EXPLAIN ANALYZE LET fully workable. In this case we want to be result of query or expression written to target variable. --- doc/src/sgml/ref/explain.sgml | 3 +- src/backend/commands/explain.c | 31 ++++++++++++-- src/backend/commands/prepare.c | 5 ++- src/backend/parser/gram.y | 3 +- src/include/commands/explain.h | 3 +- .../regress/expected/session_variables.out | 40 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 21 ++++++++++ 7 files changed, 97 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml index db9d3a8549a..caccc70658c 100644 --- a/doc/src/sgml/ref/explain.sgml +++ b/doc/src/sgml/ref/explain.sgml @@ -98,7 +98,8 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>, <command>MERGE</command>, <command>CREATE TABLE AS</command>, - or <command>EXECUTE</command> statement + <command>EXECUTE</command>, + or <command>LET</command> statement without letting the command affect your data, use this approach: <programlisting> BEGIN; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 7c0fd63b2f0..d41516454f5 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -18,6 +18,7 @@ #include "commands/createas.h" #include "commands/defrem.h" #include "commands/prepare.h" +#include "executor/svariableReceiver.h" #include "foreign/fdwapi.h" #include "jit/jit.h" #include "libpq/pqformat.h" @@ -512,8 +513,9 @@ standard_ExplainOneQuery(Query *query, int cursorOptions, } /* run it (if needed) and produce output */ - ExplainOnePlan(plan, into, es, queryString, params, queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(plan, into, query->resultVariable, es, queryString, + params, queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); } @@ -611,6 +613,25 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, else ExplainDummyGroup("Notify", NULL, es); } + else if (IsA(utilityStmt, LetStmt)) + { + LetStmt *letstmt = (LetStmt *) utilityStmt; + List *rewritten; + Query *query; + + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "SET SESSION VARIABLE\n"); + else + ExplainDummyGroup("Set Session Variable", NULL, es); + + rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query))); + + Assert(list_length(rewritten) == 1); + query = linitial_node(Query, rewritten); + ExplainOneQuery(query, + CURSOR_OPT_PARALLEL_OK, NULL, es, + pstate, params); + } else { if (es->format == EXPLAIN_FORMAT_TEXT) @@ -634,8 +655,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, * to call it. */ void -ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params, +ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, Oid targetvar, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, const BufferUsage *bufusage, const MemoryContextCounters *mem_counters) @@ -684,6 +705,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, */ if (into) dest = CreateIntoRelDestReceiver(into); + else if (OidIsValid(targetvar)) + dest = CreateVariableDestReceiver(targetvar); else if (es->serialize != EXPLAIN_SERIALIZE_NONE) dest = CreateExplainSerializeDestReceiver(es); else diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 455a03cce63..3735b5554e0 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -662,8 +662,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es, PlannedStmt *pstmt = lfirst_node(PlannedStmt, p); if (pstmt->commandType != CMD_UTILITY) - ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(pstmt, into, InvalidOid, es, query_string, paramLI, + pstate->p_queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); else ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index aad47305f20..def7dde2330 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12130,7 +12130,8 @@ ExplainableStmt: | CreateAsStmt | CreateMatViewStmt | RefreshMatViewStmt - | ExecuteStmt /* by default all are $$=$1 */ + | ExecuteStmt + | LetStmt /* by default all are $$=$1 */ ; /***************************************************************************** diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index aa5872bc154..f5b08857ae8 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -103,7 +103,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, ParseState *pstate, ParamListInfo params); -extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, +extern void ExplainOnePlan(PlannedStmt *plannedstmt, + IntoClause *into, Oid targetvar, ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 50fb478b5e9..a867fdd3e75 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1283,3 +1283,43 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; NOTICE: drop cascades to session variable xxtab.avar +CREATE VARIABLE var1 bigint; +CREATE TABLE var_tab_test_table(a int); +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); +VACUUM ANALYZE var_tab_test_table; +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index e7a4fdedae4..8a6dbffd54e 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -874,3 +874,24 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; + +CREATE VARIABLE var1 bigint; + +CREATE TABLE var_tab_test_table(a int); + +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); + +VACUUM ANALYZE var_tab_test_table; + +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be NULL +SELECT var1; + +EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be 10 +SELECT var1; + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; -- 2.47.0 [text/x-patch] 0007-GUC-session_variables_ambiguity_warning.patch (13.9K, 17-0007-GUC-session_variables_ambiguity_warning.patch) download | inline diff: From f7732df8ddd50c62f52c9c170a884f5e888239d6 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 17:55:24 +0200 Subject: [PATCH 07/21] GUC session_variables_ambiguity_warning Inside an query the session variables can be shadowed. This behaviour is by design, because this ensuring so new variables doesn't break existing queries. But this behaviour can be confusing, because shadowing is quiet. When new GUC session_variables_ambiguity_warning is on, then the warning is displayed, when some session variable can be used, but it is not used due shadowing. This feature should to help with investigation of unexpected behaviour. Note: PLpgSQL has an option that controls priority of identifier's spaces (SQL or PLpgSQL). Default behaviour doesn't allows collisions. I believe this default is the best. I cannot to implement similar very strict behaviour to session variables, because one unhappy named session variable can breaks an applications. The problems related to badly named PLpgSQL's varible is limitted just to only one routine. --- doc/src/sgml/config.sgml | 60 +++++++++++++++++++ doc/src/sgml/ddl.sgml | 10 ++++ src/backend/parser/parse_expr.c | 49 +++++++++++++-- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/parser/parse_expr.h | 1 + .../src/expected/plpgsql_session_variable.out | 33 ++++++++++ .../src/sql/plpgsql_session_variable.sql | 29 +++++++++ .../regress/expected/session_variables.out | 22 +++++++ src/test/regress/sql/session_variables.sql | 20 +++++++ 10 files changed, 230 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a84e60c09b9..d1b2175c022 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10740,6 +10740,66 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir' </listitem> </varlistentry> + <varlistentry id="guc-session-variables-ambiguity-warning" xreflabel="session_variables_ambiguity_warning"> + <term><varname>session_variables_ambiguity_warning</varname> (<type>boolean</type>) + <indexterm> + <primary><varname>session_variables_ambiguity_warning</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + When on, a warning is raised when any identifier in a query could be + used as both a column identifier, routine variable or a session + variable identifier. The default is <literal>off</literal>. + </para> + <para> + Session variables can be shadowed by column references in a query, this + is an expected behavior. Previously working queries shouldn't error out + by creating any session variable, so session variables are always shadowed + if an identifier is ambiguous. Variables should be referenced using + anunambiguous identifier without any possibility for a collision with + identifier of other database objects (column names or record fields names). + The warning messages emitted when enabling <varname>session_variables_ambiguity_warning</varname> + can help finding such identifier collision. +<programlisting> +CREATE TABLE foo(a int); +INSERT INTO foo VALUES(10); +CREATE VARIABLE a int; +LET a = 100; +SELECT a FROM foo; +</programlisting> + +<screen> + a +---- + 10 +(1 row) +</screen> + +<programlisting> +SET session_variables_ambiguity_warning TO on; +SELECT a FROM foo; +</programlisting> + +<screen> +WARNING: session variable "a" is shadowed +LINE 1: SELECT a FROM foo; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a +---- + 10 +(1 row) +</screen> + </para> + <para> + This feature can significantly increase log size, so it's disabled by + default. For testing or development environments it's recommended to + enable it if you use session variables. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-standard-conforming-strings" xreflabel="standard_conforming_strings"> <term><varname>standard_conforming_strings</varname> (<type>boolean</type>) <indexterm><primary>strings</primary><secondary>standard conforming</secondary></indexterm> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 327548fae12..ce1b1e1f599 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5359,6 +5359,16 @@ SELECT current_user_id; the schema name, columns can use table aliases, routine variables can use block labels, and routine arguments can use the routine name. </para> + + <para> + When a query contains identifiers or qualified identifiers that could be + used as both a session variable identifiers and as column identifier, + then the column identifier is preferred every time. Warnings can be + emitted when this situation happens by enabling configuration parameter <xref + linkend="guc-session-variables-ambiguity-warning"/>. User can explicitly + qualify the source object by syntax <literal>table.column</literal> or + <literal>schema.variable</literal>. + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index aba5259e027..3602c3572d7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -45,6 +45,7 @@ /* GUC parameters */ bool Transform_null_equals = false; +bool session_variables_ambiguity_warning = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -960,11 +961,51 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) * * SELECT foo.a, foo.b, foo.c FROM foo; * - * However, that is very confusing, so we disallow it. We don't try to - * identify a variable if we know that it would be shadowed. - * ----- + * However, that is very confusing, so we disallow it. + * + * When session_variables_ambiguity_warning is requested, then we + * need to identify a variable although we know, so this variable + * would be shadowed. */ - if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + if (node || (relname && crerr == CRERR_NO_COLUMN)) + { + /* + * In this path we just try (if it is wanted) detect if session + * variable is shadowed. + */ + if (session_variables_ambiguity_warning) + { + /* + * The AccessShareLock is created on related session variable. + * The lock will be kept for the whole transaction. + */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, true); + + if (OidIsValid(varid)) + { + /* this path will ending by WARNING. Unlock variable first */ + UnlockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (node) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is shadowed", + NameListToString(cref->fields)), + errdetail("Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name."), + parser_errposition(pstate, cref->location))); + else + /* session variable is shadowed by RTE */ + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s.%s\" is shadowed", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("Session variables can be shadowed by tables or table's aliases with the same name."), + parser_errposition(pstate, cref->location))); + } + } + } + else { /* takes an AccessShareLock on the session variable */ varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 8a67f01200c..efb65b02380 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1595,6 +1595,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_ambiguity_warning", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when reference to a session variable is ambiguous."), + NULL + }, + &session_variables_ambiguity_warning, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 39a3ac23127..992dda3c644 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -714,6 +714,7 @@ #default_transaction_read_only = off #default_transaction_deferrable = off #session_replication_role = 'origin' +#session_variables_ambiguity_warning = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 9b46dfd9ecc..0d64b300f8a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -17,6 +17,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; +extern PGDLLIMPORT bool session_variables_ambiguity_warning; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ab779900596..a6523f7afe2 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -354,6 +354,39 @@ $$; NOTICE: session variable is 100 NOTICE: plpgsql variable is 1000 NOTICE: variable is 1000 +-- againt with session_variables_ambiguity_warning(on) +SET session_variables_ambiguity_warning TO on; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +WARNING: session variable "plpgsql_sv_var1" is shadowed +LINE 1: plpgsql_sv_var1 + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. +QUERY: plpgsql_sv_var1 +NOTICE: variable is 1000 +SET session_variables_ambiguity_warning TO off; DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted CREATE VARIABLE plpgsql_sv_v text; diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql index a3cc264cf38..9b8e90e81fa 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -260,6 +260,35 @@ BEGIN END; $$; +-- againt with session_variables_ambiguity_warning(on) + +SET session_variables_ambiguity_warning TO on; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +SET session_variables_ambiguity_warning TO off; + DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 85771032de3..50fb478b5e9 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1261,3 +1261,25 @@ SELECT var1; (1 row) DROP VARIABLE var1, var2; +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; +CREATE VARIABLE xxtab.avar int; +CREATE TABLE public.xxtab(avar int); +INSERT INTO public.xxtab VALUES(1); +LET xxtab.avar = 20; +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; +WARNING: session variable "xxtab.avar" is shadowed +LINE 1: SELECT xxtab.avar FROM public.xxtab; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + avar +------ + 1 +(1 row) + +SET session_variables_ambiguity_warning TO off; +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; +NOTICE: drop cascades to session variable xxtab.avar diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index d8397573eeb..e7a4fdedae4 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -854,3 +854,23 @@ BEGIN; DROP VARIABLE var1; ROLLBACK; SELECT var1; DROP VARIABLE var1, var2; + +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; + +CREATE VARIABLE xxtab.avar int; + +CREATE TABLE public.xxtab(avar int); + +INSERT INTO public.xxtab VALUES(1); + +LET xxtab.avar = 20; + +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; + +SET session_variables_ambiguity_warning TO off; + +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; -- 2.47.0 [text/x-patch] 0006-plpgsql-tests.patch (16.9K, 18-0006-plpgsql-tests.patch) download | inline diff: From c47353ac91f7e0849c61c3907e80e0dade9422cf Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 06/21] plpgsql tests --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 382 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 289 +++++++++++++ 4 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ab779900596 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,382 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; +LET plpgsql_sv_var = pi(); +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; +NOTICE: value of session variable is 3.14159265358979 +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; +SELECT plpgsql_sv_var AS "pi_multiply_2"; + pi_multiply_2 +------------------ + 6.28318530717959 +(1 row) + +DROP VARIABLE plpgsql_sv_var; +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; +-- execute in a transaction +BEGIN; +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +END; +-- execute outside of a transaction +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP FUNCTION test_func(); +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +SET ROLE TO regress_var_owner_role; +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_reader_role; +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail +SELECT var_read_func(); +ERROR: permission denied for session variable plpgsql_sv_var1 +CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" +PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should be ok +SELECT var_read_func(); +NOTICE: 10 + var_read_func +--------------- + +(1 row) + +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +DROP VARIABLE plpgsql_sv_var1; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail, but not crash +SELECT var_read_func(); +ERROR: column "plpgsql_sv_var1" does not exist +LINE 1: plpgsql_sv_var1 + ^ +QUERY: plpgsql_sv_var1 +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +DROP FUNCTION var_read_func; +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_int(1); + inc_var_int +------------- + 1 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 2 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 3 +(1 row) + +SELECT inc_var_int(1) FROM generate_series(1,10); + inc_var_int +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +LET plpgsql_sv_var2 = 0.0; +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_num(1.0); + inc_var_num +------------- + 1 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 2 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 3 +(1 row) + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + inc_var_num +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +NOTICE: variable is 1000 +DROP VARIABLE plpgsql_sv_var1; +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +select ffunc(); + ffunc +------- + abc +(1 row) + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); +DROP VARIABLE plpgsql_sv_v; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 3dd734b776b..3905c0b6d45 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..a3cc264cf38 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,289 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; + +LET plpgsql_sv_var = pi(); + +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; + +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; + +SELECT plpgsql_sv_var AS "pi_multiply_2"; + +DROP VARIABLE plpgsql_sv_var; + +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; + +-- execute in a transaction +BEGIN; +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); +END; + +-- execute outside of a transaction +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; + +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +SELECT test_func(); + +DROP FUNCTION test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; + +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +SET ROLE TO regress_var_owner_role; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_reader_role; + +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should be ok +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; + +DROP VARIABLE plpgsql_sv_var1; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail, but not crash +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION var_read_func; + +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; + +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; + +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_int(1); +SELECT inc_var_int(1); +SELECT inc_var_int(1); + +SELECT inc_var_int(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; + +LET plpgsql_sv_var2 = 0.0; + +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; + +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +DROP VARIABLE plpgsql_sv_var1; + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; + +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +select ffunc(); + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); + +DROP VARIABLE plpgsql_sv_v; -- 2.47.0 [text/x-patch] 0005-memory-cleaning-after-DROP-VARIABLE.patch (20.8K, 19-0005-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From ce9412459d35712a5abb37f6e7a8520641125940 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 09:31:28 +0100 Subject: [PATCH 05/21] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 153 +++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../regress/expected/session_variables.out | 217 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 120 ++++++++++ 8 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index ef89594a268..c7369e2b2ac 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/builtins.h" @@ -254,4 +255,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 668b8189a6c..21c7a9517b5 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" @@ -67,6 +68,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -83,6 +92,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -114,6 +134,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -167,6 +219,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -196,6 +309,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -319,22 +434,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -395,6 +530,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 443afbafd4a..3dab8ae2a48 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..0a5579dc7ce --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 143109aa4da..7453685d046 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -115,3 +115,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..c864fee4006 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT myvar; } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2c4760fb687..85771032de3 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1044,3 +1044,220 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; +-- should fail +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + Ahoj +(1 row) + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + var1 +------ + Ahoj +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1, var2; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 979b9029e58..d8397573eeb 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -734,3 +734,123 @@ DISCARD VARIABLES; -- should be zero again SELECT count(*) FROM pg_session_variables(); + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; + +-- should fail +SELECT var1; + +ROLLBACK; + +-- should be ok +SELECT var1; + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; +ROLLBACK; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; +COMMIT; +-- should be ok +SELECT var1; + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1, var2; -- 2.47.0 [text/x-patch] 0004-DISCARD-VARIABLES.patch (9.6K, 20-0004-DISCARD-VARIABLES.patch) download | inline diff: From 86c18d7defbe281e51a42cb0f190d265a5b6b5be Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 28 Jan 2024 20:54:20 +0100 Subject: [PATCH 04/21] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 +++- src/backend/commands/discard.c | 6 ++ src/backend/commands/session_variable.c | 28 ++++++++- src/backend/parser/gram.y | 6 ++ src/backend/tcop/utility.c | 3 + src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../regress/expected/session_variables.out | 63 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 36 +++++++++++ 11 files changed, 158 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 92d983ac748..d06ebaba6f4 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 13ab9575fa9..668b8189a6c 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -94,7 +94,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -661,3 +667,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8ae368c513c..aad47305f20 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2077,7 +2077,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 69d2f7e1ced..970451460c2 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2962,6 +2962,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec572815c94..3ef4776d98a 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4083,7 +4083,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index b3f03c65827..443afbafd4a 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expect extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 778db6ad4f2..04ab22bd492 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3988,6 +3988,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index a921af2486f..bd7964aea6e 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a2cb35e3d7d..2c4760fb687 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -981,3 +981,66 @@ DROP VARIABLE :"DBNAME".:"DBNAME".b; DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + var1 +------- + Hello +(1 row) + +DISCARD ALL; +SELECT var1; + var1 +------ + +(1 row) + +LET var1 = 'AHOJ'; +SELECT var1; + var1 +------ + AHOJ +(1 row) + +DISCARD VARIABLES; +SELECT var1; + var1 +------ + +(1 row) + +DROP VARIABLE var1; +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS varchar; +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +LET var1 = 'AHOJ'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DISCARD VARIABLES; +-- should be zero again +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 6445479d22d..979b9029e58 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -698,3 +698,39 @@ DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; + +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + +DISCARD ALL; +SELECT var1; + +LET var1 = 'AHOJ'; +SELECT var1; + +DISCARD VARIABLES; +SELECT var1; + +DROP VARIABLE var1; + +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS varchar; + +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + +LET var1 = 'AHOJ'; + +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + +DISCARD VARIABLES; + +-- should be zero again +SELECT count(*) FROM pg_session_variables(); -- 2.47.0 [text/x-patch] 0003-function-pg_session_variables-for-cleaning-tests.patch (4.3K, 21-0003-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From c2036e9ca0936a31623e8dbe05892d3af3ea5010 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 03/21] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 92 +++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 +++ 2 files changed, 100 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 548d9835c0d..13ab9575fa9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -569,3 +569,95 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 41f1121d19f..e943a74846f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12405,4 +12405,12 @@ proargtypes => 'int2', prosrc => 'gist_stratnum_identity' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.47.0 [text/x-patch] 0002-Storage-for-session-variables-and-SQL-interface.patch (145.2K, 22-0002-Storage-for-session-variables-and-SQL-interface.patch) download | inline diff: From 9378625d8f2ac2c22cd642d9d92a1025846af9df Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Thu, 6 Jul 2023 08:29:21 +0200 Subject: [PATCH 02/21] Storage for session variables and SQL interface Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. The identifiers of session variables should always be shadowed by possible column identifiers: we don't want to break an application by creating some badly named session variable. The limits of this patch (solved by other patches): - session variables block parallel execution - session variables blocks simple expression evaluation (in plpgsql) - SQL functions with session variables are not inlined - CALL statement is not supported (usage of direct access to express executor) - EXECUTE statement is not supported (usage of direct access to express executor) - memory used by dropped session variables is not released Implementations of EXPLAIN LET and PREPARE LET statements are in separate patches (for better readability) --- doc/src/sgml/catalogs.sgml | 13 + doc/src/sgml/ddl.sgml | 32 + doc/src/sgml/parallel.sgml | 6 + doc/src/sgml/plpgsql.sgml | 14 + doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 1 + doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++ doc/src/sgml/reference.sgml | 1 + src/backend/catalog/dependency.c | 5 + src/backend/catalog/namespace.c | 315 ++++++ src/backend/catalog/pg_variable.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/prepare.c | 8 + src/backend/commands/session_variable.c | 571 +++++++++++ src/backend/executor/Makefile | 1 + src/backend/executor/execExpr.c | 36 + src/backend/executor/execMain.c | 57 ++ src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 201 ++++ src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 9 + src/backend/optimizer/plan/setrefs.c | 135 ++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 35 +- src/backend/parser/analyze.c | 271 ++++- src/backend/parser/gram.y | 49 +- src/backend/parser/parse_agg.c | 9 + src/backend/parser/parse_expr.c | 220 ++++- src/backend/parser/parse_func.c | 2 + src/backend/tcop/dest.c | 7 + src/backend/tcop/pquery.c | 3 + src/backend/tcop/utility.c | 16 + src/backend/utils/adt/ruleutils.c | 46 + src/backend/utils/cache/plancache.c | 41 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/namespace.h | 2 + src/include/catalog/pg_variable.h | 8 + src/include/commands/session_variable.h | 32 + src/include/executor/execdesc.h | 4 + src/include/executor/svariableReceiver.h | 22 + src/include/nodes/execnodes.h | 16 + src/include/nodes/parsenodes.h | 17 + src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 4 +- src/include/nodes/primnodes.h | 6 + src/include/optimizer/planmain.h | 2 + src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 3 + src/include/tcop/cmdtaglist.h | 1 + src/include/tcop/dest.h | 1 + src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 925 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 639 ++++++++++++ src/tools/pgindent/typedefs.list | 5 + 58 files changed, 3916 insertions(+), 23 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/include/executor/svariableReceiver.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bdd2ca0152f..0c15d56dd42 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9785,6 +9785,19 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>XLogRecPtr</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varname</structfield> <type>name</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2e3f0b95fb0..327548fae12 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5317,6 +5317,38 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; VARIABLE</command> command. </para> + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT READ ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); +SELECT current_user_id; +</programlisting> + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> + <para> Inside a query or an expression, a session variable can be <quote>shadowed</quote> by a column with the same name. Similarly, the diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 78e4983139b..bfbff9ab74e 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6034,6 +6034,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index d87570e7d8b..d2036351e53 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index ec165ab96fc..05bf67e3411 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -145,6 +145,7 @@ SELECT var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..f6640576bee --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>session_variable</literal></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>sql_expression</literal></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + + <para> + Example: +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c9d686665de..148719e490e 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 86e2258657d..487353ef2eb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3412,6 +3412,321 @@ LookupVariable(const char *nspname, return varoid; } +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} + +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or "variable"."field". + * Check both variants, and returns InvalidOid with + * not_unique flag, when both interpretations are + * possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * In this case, the field1 should be variable name. But + * direct unboxing of composite session variables is not + * supported now, and then we don't need to try lookup + * related variable. + * + * Unboxing is supported by syntax (var).* + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.field. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else if (OidIsValid(varid)) + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index e3a861c8cba..ef89594a268 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -26,6 +26,7 @@ #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/pg_lsn.h" #include "utils/syscache.h" static ObjectAddress create_variable(const char *varName, @@ -101,6 +102,7 @@ create_variable(const char *varName, varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 48f7348f91c..1cfaeca51ea 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -50,6 +50,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 6dd00a4abde..ca621be5ecb 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -38,6 +38,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index a93f970a292..455a03cce63 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,6 +338,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..548d9835c0d --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,571 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "rewrite/rewriteHandler.h" +#include "storage/lmgr.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Wrapper around SetSessionVariable with permission checks. + */ +void +SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull) +{ + AclResult aclresult; + + /* is the caller allowed to update the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + SetSessionVariable(varid, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull, Oid *typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message ws processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + Assert(svar); + + *typid = svar->typid; + + return copy_session_variable_value(svar, isNull); +} + +/* + * Returns a copy of the value of the session variable after checking if the + * type is the same as "expected_typid". The caller is responsible for + * permission checks. + */ +Datum +GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + Assert(svar && svar->is_valid); + + if (expected_typid != svar->typid) + elog(ERROR, "type of variable \"%s.%s\" is different than expected", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)); + + return copy_session_variable_value(svar, isNull); +} + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L, true); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 8f7a5340059..6dc8c562bec 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -34,6 +34,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -991,6 +992,41 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.d.param.paramtype = param->paramtype; ExprEvalPushStep(state, &scratch); break; + + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; + case PARAM_EXTERN: /* diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 5ca856fd279..21e938a6227 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/nodeSubplan.h" @@ -195,6 +197,61 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + ListCell *lc; + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach(lc, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + Oid varid = lfirst_oid(lc); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].varid = varid; + estate->es_session_variables[i].value = GetSessionVariable(varid, + &estate->es_session_variables[i].isnull, + &estate->es_session_variables[i].typid); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index b511a429adf..b6662c6e8fc 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c4cff44ecf4 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,201 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. Because a received tuple can have + * deleted attributes, we need to find the first non-deleted attribute + * (field "slot_offset"). The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int slot_offset; /* position of non-deleted attribute */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + int natts = typeinfo->natts; + int outcols = 0; + int i; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + for (i = 0; i < natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(typeinfo, i); + Oid typid; + Oid collid; + int32 typmod; + + if (attr->attisdropped) + continue; + + if (++outcols > 1) + continue; + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + /* + * Double check - the type and typmod of target variable should be the + * same as the type and typmod of assignment expression. The + * expression should be wrapped by a cast to the target type/typmod. + */ + if (attr->atttypid != typid || + (attr->atttypmod >= 0 && + attr->atttypmod != typmod)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("target session variable is of type %s" + " but expression is of type %s", + format_type_with_typemod(typid, typmod), + format_type_with_typemod(attr->atttypid, + attr->atttypmod)))); + + myState->need_detoast = attr->attlen == -1; + myState->slot_offset = i; + } + + if (outcols != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + outcols, + outcols))); + + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[myState->slot_offset]; + isnull = slot->tts_isnull[myState->slot_offset]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3060847b133..8fc67f08d76 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4341,6 +4341,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 1f78dc3d530..aee3a0d1ad4 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -325,6 +325,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->lastPlanNodeId = 0; glob->transientPlan = false; glob->dependsOnRole = false; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -559,6 +560,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + result->sessionVariables = glob->sessionVariables; result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -729,6 +731,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 91c7c4fe2fe..1fa2ecf5c20 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1310,6 +1313,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -1958,8 +2005,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2049,6 +2097,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2058,6 +2113,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2076,6 +2135,41 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + ListCell *lc; + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach(lc, root->glob->sessionVariables) + { + if (lfirst_oid(lc) == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2137,7 +2231,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2159,7 +2256,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3544,6 +3642,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list, or as the target of a LET statement. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries @@ -3629,9 +3746,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3654,6 +3771,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, (void *) context, 0); diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 33b10d72cb5..9f698a46d87 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1410,6 +1410,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8e39795e245..a5452c0c9e4 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -935,6 +936,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2389,6 +2397,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2515,6 +2524,29 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariableWithTypeCheck(param->paramvarid, + &isnull, + param->paramtype); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4718,7 +4750,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 506e0631615..81b8b0a633d 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,9 +25,12 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -51,6 +54,7 @@ #include "utils/backend_status.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -83,6 +87,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -414,6 +420,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -493,6 +500,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -545,6 +557,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -653,6 +666,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1079,6 +1093,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1544,6 +1559,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1770,12 +1786,242 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + AclResult aclresult; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type", + format_type_be(typid), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, stmt->location))); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names)); + + pstate->p_expr_kind = EXPR_KIND_LET_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach(lc, selectQuery->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Param *param; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s," + " but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. This value is used multiple times: by + * the query rewriter (for getting related defexpr), by planner (for + * acquiring an AccessShareLock on target variable), and by the executor + * (we need to know the target variable ID). + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * transformSetOperationStmt - * transforms a set-operations tree @@ -2021,6 +2267,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2496,6 +2743,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2563,6 +2811,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2750,9 +2999,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Transform the target reference. Typically we will get back a Param * node, but there's no reason to be too picky about its type. + * + * Session variables should not be used as target of a PL/pgSQL assign + * statement. So we should use a dedicated expression kind and disallow + * session variables there. The dedicated context allows to eliminate + * undesirable warnings about the possibility of a target PL/pgSQL variable + * shadowing a session variable. */ target = transformExpr(pstate, (Node *) cref, - EXPR_KIND_UPDATE_TARGET); + EXPR_KIND_ASSIGN_TARGET); targettype = exprType(target); targettypmod = exprTypmod(target); targetcollation = exprCollation(target); @@ -2794,6 +3049,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) */ type_id = exprType((Node *) tle->expr); + /* + * For indirection processing and additional casts we can use expr_kind + * EXPR_KIND_UPDATE_TARGET. + */ pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; if (indirection) @@ -2936,6 +3195,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ @@ -3209,6 +3470,14 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); + /* + * The arguments of CALL statement are evaluated by a direct expression + * executor call. This path is unsupported yet, so block it. It will be + * enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 89fa23c2dd4..8ae368c513c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -293,7 +293,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -443,6 +443,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list + let_target %type <node> opt_routine_body %type <groupclause> group_clause @@ -734,7 +735,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1081,6 +1082,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12770,6 +12772,47 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET let_target '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = $2; + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + +let_target: + ColId opt_indirection + { + $$ = list_make1(makeString($1)); + if ($2) + $$ = list_concat($$, + check_indirection($2, yyscanner)); + } + ; + /***************************************************************************** * * QUERY: @@ -17840,6 +17883,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18451,6 +18495,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index efa730c1676..a7343001f21 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -580,6 +580,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -970,6 +975,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c2806297aa4..aba5259e027 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -17,6 +17,7 @@ #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -33,11 +34,13 @@ #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" /* GUC parameters */ @@ -106,6 +109,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* @@ -499,6 +505,89 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_POLICY: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_COPY_WHERE: + case EXPR_KIND_LET_TARGET: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + result = false; + break; + } + + return result; +} + /* * Transform a ColumnRef. * @@ -575,6 +664,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: /* okay */ break; @@ -847,8 +938,61 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) } /* - * Throw error if no translation found. + * There are contexts where session variables are not allowed. We don't + * need to identify session variables in such a context, but identifying + * them allows us to raise meaningful error messages like "you cannot use + * session variables here". */ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + { + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* ----- + * Session variables are shadowed by columns, routine variables or + * routine arguments. We certainly don't want to use a session variable + * when it is exactly shadowed, but a RTE like this is conceivable: + * + * CREATE TYPE t AS (c int); + * CREATE VARIABLE foo AS t; + * CREATE TABLE foo(a int, b int); + * + * SELECT foo.a, foo.b, foo.c FROM foo; + * + * However, that is very confusing, so we disallow it. We don't try to + * identify a variable if we know that it would be shadowed. + * ----- + */ + if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + { + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + node = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, cref->location); + } + } + } + + /* throw an error if no translation was found */ if (node == NULL) { switch (crerr) @@ -880,6 +1024,72 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable", attrname), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + static Node * transformParamRef(ParseState *pstate, ParamRef *pref) { @@ -1815,6 +2025,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_LET_TARGET: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -1858,6 +2069,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_ASSIGN_TARGET: + err = _("cannot use subquery as target of assign statement"); + break; /* * There is intentionally no default: case here, so that the @@ -3197,6 +3411,10 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_ASSIGN_TARGET: + return "ASSIGN"; + case EXPR_KIND_LET_TARGET: + return "LET"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9b23344a3b1..9aa4f60768b 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2656,6 +2656,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) err = _("set-returning functions are not allowed in column generation expressions"); break; case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: errkind = true; break; diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index 96f80b30463..dac78ba5cce 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 0c45fcf318f..85b809cb071 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -86,6 +86,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt, qd->queryEnv = queryEnv; qd->instrument_options = instrument_options; /* instrumentation wanted? */ + qd->num_session_variables = 0; + qd->session_variables = NULL; + /* null these fields until set by ExecutorStart */ qd->tupDesc = NULL; qd->estate = NULL; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 217c6028497..69d2f7e1ced 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -49,6 +49,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -235,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1069,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2213,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2415,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3304,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a39068d1bf1..e7f74947cde 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -528,6 +529,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8617,6 +8619,14 @@ get_parameter(Param *param, deparse_context *context) return; } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Alternatively, maybe it's a subplan output, which we print as a * reference to the subplan. (We could drill down into the subplan and @@ -13399,6 +13409,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 5af1a168ec2..7e1e9e28f12 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -162,6 +163,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -1903,18 +1905,33 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, (void *) &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -1929,6 +1946,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already @@ -2060,7 +2091,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index e48a86be54b..eaae0c5a933 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 293648e30c7..ec572815c94 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1214,8 +1214,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4629,6 +4629,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 18ea6ac83a5..def691dafb3 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -171,7 +171,9 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror); extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 3810e040f28..db593cd5bed 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -35,6 +35,14 @@ CATALOG(pg_variable,9222,VariableRelationId) /* OID of entry in pg_type for variable's type */ Oid vartype BKI_LOOKUP(pg_type); + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + /* variable name */ NameData varname; diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..b3f03c65827 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,32 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "nodes/params.h" +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" +#include "tcop/cmdtag.h" +#include "utils/queryenvironment.h" + +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); +extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid); + +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + +#endif diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h index 0a7274e26c8..d1b2f59e0cf 100644 --- a/src/include/executor/execdesc.h +++ b/src/include/executor/execdesc.h @@ -48,6 +48,10 @@ typedef struct QueryDesc EState *estate; /* executor's query-wide state */ PlanState *planstate; /* tree of per-plan-node state */ + /* reference to session variables buffer */ + int num_session_variables; + SessionVariableValue *session_variables; + /* This field is set by ExecutorRun */ bool already_executed; /* true if previously executed */ diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 182a6956bb0..b4464edc22a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -617,6 +617,18 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + Oid varid; + Oid typid; + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -669,6 +681,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0e08296b438..778db6ad4f2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -142,6 +142,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -162,6 +165,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -2100,6 +2105,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + int location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index add0f9e45fc..ea1f76d0fbb 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -163,6 +163,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -508,6 +511,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 52f29bcdb69..4549366cf59 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -49,7 +49,7 @@ typedef struct PlannedStmt NodeTag type; - CmdType commandType; /* select|insert|update|delete|merge|utility */ + CmdType commandType; /* select|let|insert|update|delete|merge|utility */ uint64 queryId; /* query identifier (copied from Query) */ @@ -94,6 +94,8 @@ typedef struct PlannedStmt Node *utilityStmt; /* non-null if this is utility stmt */ + List *sessionVariables; /* OIDs for PARAM_VARIABLE Params */ + /* statement location in source string (copied from Query) */ ParseLoc stmt_location; /* start location, or -1 if unknown */ ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index b0ef1952e8f..4b2424d6d34 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -361,6 +361,9 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramid holds the variable's OID). */ typedef enum ParamKind { @@ -368,6 +371,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -380,6 +384,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of session variable if it is used */ + Oid paramvarid; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 93137261e48..61341c50cf7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -124,4 +124,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 56670e65b11..aefe51f335b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -256,6 +256,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 2375e95c107..c11fdb3d970 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -82,6 +82,8 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ + EXPR_KIND_LET_TARGET, /* LET target */ } ParseExprKind; @@ -244,6 +246,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 9465df7b2ff..a921af2486f 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index a3d521b6f97..38cc87f4e7a 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..1361a8a8480 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,7 +8099,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index d0aff56a01d..a2cb35e3d7d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -56,3 +56,928 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; +-- check access rights +CREATE ROLE regress_noowner; +CREATE VARIABLE var1 AS int; +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; +LET var1 = 10; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +-- should fail +SET ROLE TO regress_noowner; +SELECT var1; +ERROR: permission denied for session variable var1 +SELECT sqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: SQL function "sqlfx" statement 1 +SELECT plpgsqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "$1 + var1" +PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +-- should be ok +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; +-- should be ok +SET ROLE TO regress_noowner; +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); +DROP ROLE regress_noowner; +-- use variables inside views +CREATE VARIABLE var1 AS numeric; +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- start a new session +\c +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- should fail, dependency +DROP VARIABLE var1; +ERROR: cannot drop session variable var1 because other objects depend on it +DETAIL: view test_view depends on session variable var1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view test_view +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; +LET var1 = 'str1'; +LET var2 = 'str2'; +SELECT sqlfx1(sqlfx2('Hello')); + sqlfx1 +------------------- + str1, str2, Hello +(1 row) + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + QUERY PLAN +------------------------------------------------------ + Result + Output: sqlfx1(sqlfx2('Hello'::character varying)) +(2 rows) + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; +set plan_cache_mode TO force_generic_plan; +LET var1 = 3.14; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 3.14 +(1 row) + +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 6.28 +(1 row) + +DROP VARIABLE var1; +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 9.42 +(1 row) + +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 12.56 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); +set plan_cache_mode TO DEFAULT; +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; +NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} +DROP VARIABLE var1; +DROP VARIABLE var2; +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; +-- should not crash (but is not supported yet) +CALL p(v); +ERROR: session variable cannot be used as an argument +DO $$ BEGIN CALL p(v); END $$; +ERROR: session variable cannot be used as an argument +CONTEXT: SQL statement "CALL p(v)" +PL/pgSQL function inline_code_block line 1 at CALL +DROP PROCEDURE p(int); +DROP VARIABLE v; +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; +-- should fail +EXECUTE ptest(v); +ERROR: session variable cannot be used as an argument +DEALLOCATE ptest; +DROP VARIABLE v; +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; +-- should fail +LET var1 = pi(); +ERROR: session variable "var1" doesn't exist +LINE 1: LET var1 = pi(); + ^ +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + var1 +------------------ + 3.14159265358979 +(1 row) + +SET search_path TO svartest; +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + var1 +------------------ + 13.1415926535898 +(1 row) + +RESET search_path; +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to session variable svartest.var1 +CREATE VARIABLE var1 AS text; +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; +SELECT var1; + var1 +------- + hello +(1 row) + +DROP VARIABLE var1; +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; +-- should fail +SELECT var1; +ERROR: domain int_domain does not allow null values +-- should be ok +LET var1 = 1000; +SELECT var1; + var1 +------ + 1000 +(1 row) + +-- should fail +LET var1 = 10; +ERROR: value for domain int_domain violates check constraint "int_domain_check" +-- should fail +LET var1 = NULL; +ERROR: domain int_domain does not allow null values +-- note - domain defaults are not supported yet (like PLpgSQL) +DROP VARIABLE var1; +DROP DOMAIN int_domain; +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + var1 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +DROP VARIABLE var1; +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + var1 +------ + 100 +(1 row) + +SET search_path to public, svartest; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table foo +drop cascades to session variable var1 +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +LET var2[var1] = 0; +SELECT var2; + var2 +----------- + [2:2]={0} +(1 row) + +DROP VARIABLE var1, var2; +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +SELECT var2; + var2 +------ + {} +(1 row) + +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" +LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x; + ^ +-- should be ok +LET var1 = generate_series(1, 1); +-- should fail +LET var1 = generate_series(1, 2); +ERROR: expression returned more than one row +LET var1 = generate_series(1, 0); +ERROR: expression returned no rows +DROP VARIABLE var1; +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); +SELECT v1; + v1 +------------ + (1,2,3.00) +(1 row) + +SELECT v2; + v2 +--------------- + (10,20,31.40) +(1 row) + +SELECT (v1).*; + x | y | z +---+---+------ + 1 | 2 | 3.00 +(1 row) + +SELECT (v2).*; + x | y | z +----+----+------- + 10 | 20 | 31.40 +(1 row) + +SELECT v1.x + v1.z; + ?column? +---------- + 4.00 +(1 row) + +SELECT v2.x + v2.z; + ?column? +---------- + 41.40 +(1 row) + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should fail +SELECT v2.x; +ERROR: permission denied for session variable v2 +SET ROLE TO DEFAULT; +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; +CREATE TYPE t1 AS (a int, b numeric, c text); +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; + v1 +---------------------------- + (1,3.14159265358979,hello) +(1 row) + +LET v1.b = 10.2222; +SELECT v1; + v1 +------------------- + (1,10.2222,hello) +(1 row) + +-- should fail, attribute doesn't exist +LET v1.x = 10; +ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +LINE 1: LET v1.x = 10; + ^ +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; +ERROR: assignment expression returned 3 columns +LINE 1: LET v1 = (NULL::t1).*; + ^ +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + v1 +------------- + (1,10.2222) +(1 row) + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + v1 +-------------- + (1,10.2222,) +(1 row) + +LET v1 = (10, 10.3, 20); +SELECT v1; + v1 +-------------- + (10,10.3,20) +(1 row) + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + v1 +--------- + (10,20) +(1 row) + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; +ERROR: cannot alter type "t1" because session variable "public.v1" uses it +DROP VARIABLE v1; +DROP TYPE t1; +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + var1 +---------------------------------- + (10,3.14159265358979,05-26-2023) +(1 row) + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; +ERROR: cannot alter table "svar_test" because session variable "public.var1" uses it +-- should fail +DROP TABLE svar_test; +ERROR: cannot drop table svar_test because other objects depend on it +DETAIL: session variable var1 depends on type svar_test +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VARIABLE var1; +DROP TABLE svar_test; +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + var1 +------------ + {10.1,2.1} +(1 row) + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; +ERROR: set-returning functions are not allowed in LET +LINE 1: LET var1[generate_series(1,3)] = 100; + ^ +DROP VARIABLE var1; +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; + var1 +-------------------- + (10.2,"{0.0,0.0}") +(1 row) + +LET var1.b[1] = 10.3; +SELECT var1; + var1 +--------------------- + (10.2,"{10.3,0.0}") +(1 row) + +DROP VARIABLE var1; +DROP TYPE t1; +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + count +------- + 100 +(1 row) + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + QUERY PLAN +----------------------------------- + Aggregate + -> Seq Scan on svar_test + Filter: ((a % 10) = zero) +(3 rows) + +LET zero = (SELECT count(*) FROM svar_test); +-- result should be 1000 +SELECT zero; + zero +------ + 1000 +(1 row) + +DROP VARIABLE zero; +DROP TABLE svar_test; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO on; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO off; +DROP VIEW var1view; +DROP VARIABLE var1; +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; + id | v +----+----- + 1 | 100 +(1 row) + +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+----- + 1 | 200 +(1 row) + +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+--- +(0 rows) + +DROP TABLE svar_test; +DROP VARIABLE varid; +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + relname +---------- + pg_class +(1 row) + +DROP VARIABLE var1; +CREATE TABLE xxtab(avar int); +INSERT INTO xxtab VALUES(333); +CREATE TYPE xxtype AS (avar int); +CREATE VARIABLE xxtab AS xxtype; +INSERT INTO xxtab VALUES(10); +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +CREATE VARIABLE public.avar AS int; +-- should be ok, see the table +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT public.avar FROM xxtab; + avar +------ + + +(2 rows) + +DROP VARIABLE xxtab; +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +DROP VARIABLE public.avar; +DROP TYPE xxtype; +DROP TABLE xxtab; +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; +CREATE TABLE public.svar(a int, b int); +INSERT INTO public.svar VALUES(10, 20); +LET public.svar = (100, 200, 300); +-- should be ok +-- show table +SELECT * FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; +CREATE TYPE ab AS (a integer, b integer); +CREATE VARIABLE v_ab AS ab; +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +CREATE SCHEMA v_ab; +CREATE VARIABLE v_ab.a AS integer; +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; +SET search_path TO public; +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); +-- should be ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; +-- should be still ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; +ERROR: session variable reference "xxx_am.b" is ambiguous +LINE 1: SELECT xxx_am.b; + ^ +-- enhanced references should be ok +SELECT public.xxx_am.b; + b +---- + 10 +(1 row) + +SELECT :"DBNAME".xxx_am.b; + b +---- + 20 +(1 row) + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); +-- we should see table +SELECT xxx_am.b FROM xxx_am; + b +---- + 10 +(1 row) + +SELECT x.b FROM xxx_am x; + b +---- + 10 +(1 row) + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; +CREATE SCHEMA :"DBNAME"; +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; +SET search_path TO :"DBNAME"; +-- should be ambiguous +SELECT :"DBNAME".b; +ERROR: session variable reference "regression.b" is ambiguous +LINE 1: SELECT "regression".b; + ^ +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; +ERROR: session variable reference "regression.regression.b" is ambiguous +LINE 1: SELECT "regression"."regression".b; + ^ +CREATE TABLE :"DBNAME"(b int); +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + b +--- +(0 rows) + +DROP TABLE :"DBNAME"; +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; +RESET search_path; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 82679cbf48e..6445479d22d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -59,3 +59,642 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; + +-- check access rights +CREATE ROLE regress_noowner; + +CREATE VARIABLE var1 AS int; + +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; + +LET var1 = 10; +-- should be ok +SELECT var1; +SELECT sqlfx(20); +SELECT sqlfx_sd(20); +SELECT plpgsqlfx(20); +SELECT plpgsqlfx_sd(20); + +-- should fail +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +-- should be ok +SELECT sqlfx_sd(20); +SELECT plpgsqlfx_sd(20); + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; + +-- should be ok +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); + +DROP ROLE regress_noowner; + +-- use variables inside views +CREATE VARIABLE var1 AS numeric; + +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- start a new session +\c + +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- should fail, dependency +DROP VARIABLE var1; + +-- should be ok +DROP VARIABLE var1 CASCADE; + +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; + +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; + +LET var1 = 'str1'; +LET var2 = 'str2'; + +SELECT sqlfx1(sqlfx2('Hello')); + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; + +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; + +set plan_cache_mode TO force_generic_plan; + +LET var1 = 3.14; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; + +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; + +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); + +set plan_cache_mode TO DEFAULT; + +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; + +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; + +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; + +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; + +-- should not crash (but is not supported yet) +CALL p(v); + +DO $$ BEGIN CALL p(v); END $$; + +DROP PROCEDURE p(int); +DROP VARIABLE v; + +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; + +-- should fail +EXECUTE ptest(v); + +DEALLOCATE ptest; +DROP VARIABLE v; + +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; + +-- should fail +LET var1 = pi(); +SELECT var1; + +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + +SET search_path TO svartest; + +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + +RESET search_path; +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS text; + +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; + +SELECT var1; + +DROP VARIABLE var1; + +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; + +-- should fail +SELECT var1; + +-- should be ok +LET var1 = 1000; +SELECT var1; + +-- should fail +LET var1 = 10; + +-- should fail +LET var1 = NULL; + +-- note - domain defaults are not supported yet (like PLpgSQL) + +DROP VARIABLE var1; +DROP DOMAIN int_domain; + +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; + +DROP VARIABLE var1; + +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + +SET search_path to public, svartest; + +SELECT var1; + +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +LET var2[var1] = 0; + +SELECT var2; + +DROP VARIABLE var1, var2; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +SELECT var2; + +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; + +-- should be ok +LET var1 = generate_series(1, 1); + +-- should fail +LET var1 = generate_series(1, 2); +LET var1 = generate_series(1, 0); + +DROP VARIABLE var1; + +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); + +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; + +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); + +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); + +SELECT v1; +SELECT v2; +SELECT (v1).*; +SELECT (v2).*; + +SELECT v1.x + v1.z; +SELECT v2.x + v2.z; + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; + +SET ROLE TO regress_var_test_role; + +-- should fail +SELECT v2.x; + +SET ROLE TO DEFAULT; + +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; + +CREATE TYPE t1 AS (a int, b numeric, c text); + +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; +LET v1.b = 10.2222; +SELECT v1; + +-- should fail, attribute doesn't exist +LET v1.x = 10; + +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; + +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + +LET v1 = (10, 10.3, 20); +SELECT v1; + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; + +DROP VARIABLE v1; +DROP TYPE t1; + +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; + +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; + +-- should fail +DROP TABLE svar_test; + +DROP VARIABLE var1; +DROP TABLE svar_test; + +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; + +DROP VARIABLE var1; + +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; +LET var1.b[1] = 10.3; +SELECT var1; + +DROP VARIABLE var1; +DROP TYPE t1; + +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; + +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + +LET zero = (SELECT count(*) FROM svar_test); + +-- result should be 1000 +SELECT zero; + +DROP VARIABLE zero; +DROP TABLE svar_test; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; + +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; + +SELECT * FROM var1view; + +SET debug_parallel_query TO on; + +SELECT * FROM var1view; + +SET debug_parallel_query TO off; + +DROP VIEW var1view; +DROP VARIABLE var1; + +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); + +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + +DROP TABLE svar_test; +DROP VARIABLE varid; + + +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + +DROP VARIABLE var1; + +CREATE TABLE xxtab(avar int); + +INSERT INTO xxtab VALUES(333); + +CREATE TYPE xxtype AS (avar int); + +CREATE VARIABLE xxtab AS xxtype; + +INSERT INTO xxtab VALUES(10); + +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + +-- should be ok +SELECT avar FROM xxtab; + +CREATE VARIABLE public.avar AS int; + +-- should be ok, see the table +SELECT avar FROM xxtab; + +-- should be ok +SELECT public.avar FROM xxtab; + +DROP VARIABLE xxtab; + +SELECT xxtab.avar FROM xxtab; + +DROP VARIABLE public.avar; + +DROP TYPE xxtype; + +DROP TABLE xxtab; + +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; + +CREATE TABLE public.svar(a int, b int); + +INSERT INTO public.svar VALUES(10, 20); + +LET public.svar = (100, 200, 300); + +-- should be ok +-- show table +SELECT * FROM public.svar; +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; + +CREATE TYPE ab AS (a integer, b integer); + +CREATE VARIABLE v_ab AS ab; + +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); + +-- we should see table +SELECT v_ab.a FROM v_ab; + +CREATE SCHEMA v_ab; + +CREATE VARIABLE v_ab.a AS integer; + +-- we should see table +SELECT v_ab.a FROM v_ab; + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; + +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; + +SET search_path TO public; + +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); + +-- should be ok +SELECT xxx_am; + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; + +-- should be still ok +SELECT xxx_am; + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; + +-- enhanced references should be ok +SELECT public.xxx_am.b; +SELECT :"DBNAME".xxx_am.b; + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); + +-- we should see table +SELECT xxx_am.b FROM xxx_am; +SELECT x.b FROM xxx_am x; + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; + +CREATE SCHEMA :"DBNAME"; + +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; + +SET search_path TO :"DBNAME"; + +-- should be ambiguous +SELECT :"DBNAME".b; + +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; + +CREATE TABLE :"DBNAME"(b int); + +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + +DROP TABLE :"DBNAME"; + +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; + +RESET search_path; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9e4f3c1444d..68e3c3c7b63 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1498,6 +1498,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey @@ -2603,6 +2604,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData @@ -2793,6 +2795,9 @@ SupportRequestRows SupportRequestSelectivity SupportRequestSimplify SupportRequestWFuncMonotonic +SVariable +SVariableData +SVariableState Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] 0001-Enhancing-catalog-for-support-session-variables-and-.patch (129.9K, 23-0001-Enhancing-catalog-for-support-session-variables-and-.patch) download | inline diff: From d5d4e1c9c3eae38f1c3de6113064c0e65179bbde Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 15:41:55 +0100 Subject: [PATCH 01/21] Enhancing catalog for support session variables and related support This patch introduces new system catalog table pg_variable. This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. Both commands are implemented by this patch. Possibility to change owner, schema or rename. Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. This patch enhancing pg_dump and psql to support session variables. The changes are related to system catalog. This patch is not short, but the code is simple. --- doc/src/sgml/catalogs.sgml | 120 +++++++++ doc/src/sgml/ddl.sgml | 31 +++ doc/src/sgml/func.sgml | 13 + doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/ref/allfiles.sgml | 3 + .../sgml/ref/alter_default_privileges.sgml | 26 +- doc/src/sgml/ref/alter_variable.sgml | 178 ++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 7 +- doc/src/sgml/ref/create_variable.sgml | 151 +++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++ doc/src/sgml/ref/grant.sgml | 6 + doc/src/sgml/ref/revoke.sgml | 7 + doc/src/sgml/reference.sgml | 3 + src/backend/catalog/Makefile | 1 + src/backend/catalog/aclchk.c | 78 ++++++ src/backend/catalog/dependency.c | 6 + src/backend/catalog/meson.build | 1 + src/backend/catalog/namespace.c | 133 +++++++++ src/backend/catalog/objectaddress.c | 121 ++++++++- src/backend/catalog/pg_shdepend.c | 2 + src/backend/catalog/pg_variable.c | 255 ++++++++++++++++++ src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/seclabel.c | 1 + src/backend/commands/tablecmds.c | 41 +++ src/backend/parser/gram.y | 105 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 16 ++ src/backend/utils/adt/acl.c | 7 + src/backend/utils/cache/lsyscache.c | 113 ++++++++ src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 + src/bin/pg_dump/pg_dump.c | 195 +++++++++++++- src/bin/pg_dump/pg_dump.h | 19 ++ src/bin/pg_dump/pg_dump_sort.c | 6 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 96 +++++++ src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 42 ++- src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/namespace.h | 4 + src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 3 + src/include/catalog/pg_variable.h | 81 ++++++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 2 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/acl.h | 1 + src/include/utils/lsyscache.h | 9 + src/test/regress/expected/dependency.out | 17 ++ src/test/regress/expected/oidjoins.out | 4 + src/test/regress/expected/psql.out | 50 ++++ .../regress/expected/session_variables.out | 58 ++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 + src/test/regress/sql/psql.sql | 21 ++ src/test/regress/sql/session_variables.sql | 61 +++++ src/tools/pgindent/typedefs.list | 4 + 65 files changed, 2365 insertions(+), 26 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h create mode 100644 src/test/regress/expected/session_variables.out create mode 100644 src/test/regress/sql/session_variables.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 59bb833f48d..bdd2ca0152f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9734,4 +9739,119 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 08155b156a5..2e3f0b95fb0 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5298,6 +5298,37 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. + </para> + + <para> + Inside a query or an expression, a session variable can be + <quote>shadowed</quote> by a column with the same name. Similarly, the + name of a function or procedure argument or a PL/pgSQL variable (see + <xref linkend="plpgsql-declarations"/>) can shadow a session variable + in the routine's body. Such collisions of identifiers can be resolved + by using qualified identifiers: Session variables can be qualified with + the schema name, columns can use table aliases, routine variables can use + block labels, and routine arguments can use the routine name. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 1a0b85bb4d7..61e5c606e21 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25736,6 +25736,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index f54f25c1c6b..d0cb53763aa 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1664,6 +1664,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 89aacec4fab..041c78d432f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -51,6 +51,10 @@ GRANT { { USAGE | CREATE } ON SCHEMAS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -83,6 +87,12 @@ REVOKE [ GRANT OPTION FOR ] ON SCHEMAS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -117,14 +127,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, and types (including domains) can be - altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), and session + variables can be altered. For this command, functions include aggregates + and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard + term for functions and procedures taken together. In earlier PostgreSQL + releases, only the word <literal>FUNCTIONS</literal> was allowed. It is not + possible to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..d87570e7d8b --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..a834c876bc9 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..ec165ab96fc --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,151 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..78ff10fcf5e 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..626c0231d0d 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -137,6 +137,13 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] | CURRENT_ROLE | CURRENT_USER | SESSION_USER + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 1589a75fd53..7a90fcfc457 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index aaf96a965e4..3620eb356c6 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,19 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach(cell, objnames) + { + RangeVar *varvar = (RangeVar *) lfirst(cell); + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +870,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1005,6 +1055,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_SCHEMA; errormsg = gettext_noop("invalid privilege type %s for schema"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1196,6 +1250,12 @@ SetDefaultACL(InternalDefaultACL *iacls) this_privileges = ACL_ALL_RIGHTS_SCHEMA; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; + default: elog(ERROR, "unrecognized object type: %d", (int) iacls->objtype); @@ -1439,6 +1499,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_NAMESPACE: iacls.objtype = OBJECT_SCHEMA; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1499,6 +1562,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2732,6 +2798,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2843,6 +2912,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("must be owner of type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("must be owner of view %s"); break; @@ -2991,6 +3063,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4258,6 +4332,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_NAMESPACE; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 0489cbabcb8..c9d686665de 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 2f3ded8a0e7..bfdb4da3301 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 30807f91904..86e2258657d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -986,6 +987,67 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + ListCell *l; + + visible = false; + foreach(l, activeSearchPath) + { + Oid namespaceId = lfirst_oid(l); + + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3351,66 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid namespaceId; + Oid varoid = InvalidOid; + ListCell *l; + + if (nspname) + { + namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach(l, activeSearchPath) + { + namespaceId = lfirst_oid(l); + + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} /* * DeconstructQualifiedName @@ -5085,3 +5207,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + + if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid))) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(VariableIsVisible(oid)); +} diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 7520a982151..2c8b9d7fd3c 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2005,16 +2027,20 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_NAMESPACE: objtype_str = "schemas"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, - DEFACLOBJ_NAMESPACE))); + DEFACLOBJ_NAMESPACE, + DEFACLOBJ_VARIABLE))); } /* @@ -2098,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2292,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2463,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3451,6 +3497,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -3803,6 +3875,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new schemas belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -4619,6 +4701,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -5725,6 +5811,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on schemas"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) @@ -5965,6 +6055,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, varname); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 753afb88453..1ea3d6db05c 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..e3a861c8cba --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,255 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +static ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + + +/* + * Creates entry in pg_variable table + */ +static ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + Acl *varacl; + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation;; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + /* we want SessionVariableCreatePostprocess to see the catalog changes. */ + CommandCounterIncrement(); + + return variable; +} + +/* + * Drop variable by OID, and register the needed session variable + * cleanup. + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index a45f3bb6b83..d4d88d11314 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -139,6 +140,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -418,6 +423,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -558,6 +564,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -640,6 +647,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -870,6 +878,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 85eec7e3947..3cce17e92af 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index dcfc1dbaffd..d03cb947e67 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 5607273bf9f..c026ab5dcb8 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index c632d1e8245..3deef69295d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6768,6 +6769,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6831,6 +6833,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 67eb96396af..89fa23c2dd4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -281,8 +282,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -775,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES + VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1042,6 +1043,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1584,6 +1586,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5210,6 +5213,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -6998,6 +7029,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -7874,6 +7906,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -7919,6 +7959,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8116,6 +8164,7 @@ defacl_privilege_target: | SEQUENCES { $$ = OBJECT_SEQUENCE; } | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -9904,6 +9953,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10265,6 +10332,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10546,6 +10631,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -17917,6 +18010,8 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18570,6 +18665,8 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 0f324ee4e31..4eaed1f8439 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4039,6 +4040,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4107,6 +4109,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4120,6 +4131,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f28bf371059..217c6028497 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -23,6 +23,7 @@ #include "catalog/namespace.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" +#include "catalog/pg_variable.h" #include "catalog/toasting.h" #include "commands/alter.h" #include "commands/async.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 2a716cc6b7f..98522a45612 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -851,6 +851,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +952,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index a85dc0d891f..b464a7f090a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -38,6 +38,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3714,3 +3715,115 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} + +/* + * Returns the type of the given session variable. + */ +Oid +get_session_variable_type(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid vartype; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + vartype = varform->vartype; + + ReleaseSysCache(tup); + + return vartype; +} + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 33d323085f5..b0451c1b903 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 5649859aa1e..50de5d0ea29 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -511,6 +511,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index 68ae2970ade..3fa61090c64 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 8c20c263c4b..6f012a1b558 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3098,6 +3098,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3647,6 +3655,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index a8c141b689d..b8d839ece41 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -363,6 +363,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5416,6 +5417,190 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + resetPQExpBuffer(query); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || dopt->dataOnly) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -10140,7 +10325,8 @@ getAdditionalACLs(Archive *fout) dobj->objType == DO_TABLE || dobj->objType == DO_PROCLANG || dobj->objType == DO_FDW || - dobj->objType == DO_FOREIGN_SERVER) + dobj->objType == DO_FOREIGN_SERVER || + dobj->objType == DO_VARIABLE) { DumpableObjectWithAcl *daobj = (DumpableObjectWithAcl *) dobj; @@ -10739,6 +10925,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_SUBSCRIPTION_REL: dumpSubscriptionTable(fout, (const SubRelInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -15188,6 +15377,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_NAMESPACE: type = "SCHEMAS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -18898,6 +19090,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index d65f5585651..f2f558b2961 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -52,6 +52,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -699,6 +700,23 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + char *varacl; + char *rvaracl; + char *initvaracl; + char *initrvaracl; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -782,5 +800,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 3c8f2eb808d..ca323171aa8 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1500,6 +1502,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "POST-DATA BOUNDARY (ID %d)", obj->dumpId); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index aa1564cd450..dd8c054a6a6 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -789,6 +789,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1674,6 +1684,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -3942,6 +3969,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4404,6 +4449,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 1f3cbb11f7c..7594f1010a6 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1077,6 +1077,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 5bfebad64d5..c52c8fd147f 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5195,6 +5195,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 273f974538e..4e15a180ea6 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 02fe5d151e0..317bc2eab6e 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -265,6 +265,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[S+] [PATTERN] list data types\n"); HELP0(" \\du[S+] [PATTERN] list roles\n"); HELP0(" \\dv[S+] [PATTERN] list views\n"); + HELP0(" \\dV [PATTERN] list session variables\n"); HELP0(" \\dx[+] [PATTERN] list extensions\n"); HELP0(" \\dX [PATTERN] list extended statistics\n"); HELP0(" \\dy[+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fad2277991d..293648e30c7 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1304,6 +1311,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1865,7 +1873,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\errverbose", "\\ev", "\\f", @@ -2556,6 +2564,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3121,7 +3132,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3931,6 +3942,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4193,6 +4211,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4394,7 +4418,7 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4416,7 +4440,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4424,7 +4449,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4460,6 +4486,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4762,7 +4790,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5228,6 +5256,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 167f91a6e3f..507f3808218 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index f70d1daba52..6eec79d2eec 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8d434d48d57..18ea6ac83a5 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,8 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index d272cdf08b4..529d53c6bb6 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -68,6 +68,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_FUNCTION 'f' /* function */ #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index cbbe8acd382..41f1121d19f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6566,6 +6566,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9221', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..3810e040f28 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +extern ObjectAddress CreateVariable(ParseState *pstate, + CreateSessionVarStmt *stmt); +extern void DropVariableById(Oid varid); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +#endif /* PG_VARIABLE_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0f9462493e3..0e08296b438 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2316,6 +2316,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3447,6 +3448,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 899d64ad55f..56670e65b11 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -484,6 +484,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 7fdcec6dd93..9465df7b2ff 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 731d84b2a93..98aab98229e 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 20446f6f836..8a3684e711f 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -134,6 +134,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -206,6 +207,14 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); +extern Oid get_session_variable_type(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 74d9ff2998d..c336f67a596 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 36dc31c16c4..88e21194711 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5930,6 +5930,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6151,6 +6175,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6385,6 +6415,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6554,6 +6590,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of relations @@ -6687,6 +6729,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6734,6 +6782,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out new file mode 100644 index 00000000000..d0aff56a01d --- /dev/null +++ b/src/test/regress/expected/session_variables.out @@ -0,0 +1,58 @@ +CREATE ROLE regress_variable_owner; +-- should be ok +CREATE VARIABLE var1 AS int; +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; +ERROR: session variable cannot be pseudo-type anyelement +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; +NOTICE: session variable "var2" does not exist, skipping +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; +NOTICE: session variable "var1" already exists, skipping +-- should fail +CREATE VARIABLE var1 AS int; +ERROR: session variable "var1" already exists +-- should be ok +DROP VARIABLE IF EXISTS var1; +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + obj_description +----------------------- + some variable comment +(1 row) + +DROP VARIABLE var1; +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +CREATE ROLE regress_variable_reader; +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; +DROP VARIABLE public.varxx; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; +\dV+ svartest.var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| + | | | | | regress_variable_reader=r/regress_variable_owner | +(1 row) + +DROP VARIABLE svartest.var1; +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 81e4222d26a..05eb784f62b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -111,7 +111,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does a reconnect which transiently uses 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index c5021fc0b13..666eddb632d 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1631,6 +1631,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1742,6 +1755,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1783,6 +1799,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1823,6 +1841,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1847,6 +1866,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1872,6 +1892,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql new file mode 100644 index 00000000000..82679cbf48e --- /dev/null +++ b/src/test/regress/sql/session_variables.sql @@ -0,0 +1,61 @@ +CREATE ROLE regress_variable_owner; + +-- should be ok +CREATE VARIABLE var1 AS int; + +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; + +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; + +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; + +-- should fail +CREATE VARIABLE var1 AS int; + +-- should be ok +DROP VARIABLE IF EXISTS var1; + +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + +DROP VARIABLE var1; + +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; + +CREATE VARIABLE svartest.var1 AS int; + +CREATE ROLE regress_variable_reader; + +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; + +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; + +DROP VARIABLE public.varxx; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; + +\dV+ svartest.var1 + +DROP VARIABLE svartest.var1; + +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 08521d51a9b..9e4f3c1444d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -535,6 +535,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext @@ -874,6 +875,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -933,6 +935,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData @@ -3079,6 +3082,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.47.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2024-11-19 21:30 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 1 sibling, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2024-11-19 21:30 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> Hi testing farms reports some issue related to introduction new parser node. I increased verbosity Regards Pavel Attachments: [text/x-patch] v20241119-2-0020-pg_restore-A-variable.patch (2.8K, 3-v20241119-2-0020-pg_restore-A-variable.patch) download | inline diff: From 7271601152ba10bde00fd54a982861cd63003273 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 21 Jul 2024 17:04:41 +0200 Subject: [PATCH 20/22] pg_restore -A, --variable possibility to restore session variable specified by name --- doc/src/sgml/ref/pg_restore.sgml | 11 +++++++++++ src/bin/pg_dump/pg_restore.c | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index b8b27e1719e..7a3a4eabe25 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -106,6 +106,17 @@ PostgreSQL documentation </listitem> </varlistentry> + <varlistentry> + <term><option>-A <replaceable class="parameter">session_variable</replaceable></option></term> + <term><option>--variable=<replaceable class="parameter">session_variable</replaceable></option></term> + <listitem> + <para> + Restore a named session variable only. Multiple session variables may + be specified with multiple <option>-A</option> switches. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-c</option></term> <term><option>--clean</option></term> diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index f2c1020d053..f916736d761 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -104,6 +104,7 @@ main(int argc, char **argv) {"trigger", 1, NULL, 'T'}, {"use-list", 1, NULL, 'L'}, {"username", 1, NULL, 'U'}, + {"variable", 1, NULL, 'A'}, {"verbose", 0, NULL, 'v'}, {"single-transaction", 0, NULL, '1'}, @@ -154,7 +155,7 @@ main(int argc, char **argv) } } - while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", + while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", cmdopts, NULL)) != -1) { switch (c) @@ -162,6 +163,11 @@ main(int argc, char **argv) case 'a': /* Dump data only */ opts->dataOnly = 1; break; + case 'A': /* vAriable */ + opts->selTypes = 1; + opts->selVariable = 1; + simple_string_list_append(&opts->variableNames, optarg); + break; case 'c': /* clean (i.e., drop) schema prior to create */ opts->dropSchema = 1; break; @@ -462,6 +468,7 @@ usage(const char *progname) printf(_("\nOptions controlling the restore:\n")); printf(_(" -a, --data-only restore only the data, no schema\n")); + printf(_(" -A, --variable=NAME restore named session variable\n")); printf(_(" -c, --clean clean (drop) database objects before recreating\n")); printf(_(" -C, --create create the target database\n")); printf(_(" -e, --exit-on-error exit on error, default is to continue\n")); -- 2.47.0 [text/x-patch] v20241119-2-0021-variable-fence-syntax-support-and-variable-fence-usa.patch (14.7K, 4-v20241119-2-0021-variable-fence-syntax-support-and-variable-fence-usa.patch) download | inline diff: From 5c9b9608fe03eafd00950abf9fdf4e118f5d196b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 19 Nov 2024 08:14:53 +0100 Subject: [PATCH 21/22] variable fence syntax support and variable fence usage guard support this patch introduces a concept of variable fence - syntax for variable reference `VARIABLE(varname)` that is not in collision with column reference. When variable fence usage guard warning is active, then usage variable without variable fence in the case, where there can be column references, the the warning is raised. initial implementation of variable fence --- src/backend/parser/gram.y | 28 ++++++- src/backend/parser/parse_expr.c | 74 ++++++++++++++++++- src/backend/parser/parse_target.c | 7 ++ src/backend/utils/adt/ruleutils.c | 12 ++- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/nodes/parsenodes.h | 10 +++ src/include/nodes/primnodes.h | 2 + src/include/parser/parse_expr.h | 1 + .../regress/expected/session_variables.out | 56 ++++++++++++++ src/test/regress/sql/session_variables.sql | 35 +++++++++ 11 files changed, 228 insertions(+), 7 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af9d640c0e2..ad66eb450b4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -519,7 +519,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -879,7 +879,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15656,6 +15656,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17042,6 +17055,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 16bf52c9854..fecf22b8b9e 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -46,6 +46,7 @@ /* GUC parameters */ bool Transform_null_equals = false; bool session_variables_ambiguity_warning = false; +bool session_variables_use_fence_warning_guard = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -81,6 +82,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -112,7 +114,7 @@ static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); static Node *makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location); + char *attrname, bool fenced, int location); /* @@ -377,6 +379,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -1030,7 +1036,20 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) node = makeParamSessionVariable(pstate, varid, typid, typmod, collid, - attrname, cref->location); + attrname, false, cref->location); + + /* + * The variable is not inside variable's fence, so raise warning + * when variable fence guard is active and the query has FROM + * clause. + */ + if (session_variables_use_fence_warning_guard && pstate->p_rtable) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is not used inside variable fence", + NameListToString(cref->fields)), + errdetail("The collision of session variable' names and column names is possible."), + parser_errposition(pstate, cref->location))); } } } @@ -1073,7 +1092,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) static Node * makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location) + char *attrname, bool fenced, int location) { Param *param; @@ -1081,6 +1100,7 @@ makeParamSessionVariable(ParseState *pstate, param->paramkind = PARAM_VARIABLE; param->paramvarid = varid; + param->paramvarfenced = fenced; param->paramtype = typid; param->paramtypmod = typmod; param->paramcollid = collid; @@ -1156,6 +1176,54 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, true, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 6af6b801ad4..9d4adc0f5e9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2038,6 +2038,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index e7f74947cde..e218c7118b8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8622,8 +8622,16 @@ get_parameter(Param *param, deparse_context *context) /* translate paramvarid to session variable name */ if (param->paramkind == PARAM_VARIABLE) { - appendStringInfo(context->buf, "%s", - generate_session_variable_name(param->paramvarid)); + if (param->paramvarfenced) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + } + else + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + } return; } diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index efb65b02380..100dd26b286 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1604,6 +1604,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_use_fence_warning_guard", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when variable is not used inside variable fence."), + NULL + }, + &session_variables_use_fence_warning_guard, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 992dda3c644..512fe9e334d 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -715,6 +715,7 @@ #default_transaction_deferrable = off #session_replication_role = 'origin' #session_variables_ambiguity_warning = off +#session_variables_use_fence_warning_guard = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a4372f184df..97ed845e073 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -312,6 +312,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6174a5562c5..2e5665ffebd 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -388,6 +388,8 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of session variable if it is used */ Oid paramvarid; + /* true when variable is used inside an fence */ + bool paramvarfenced; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index e5aebf99b4a..d1f2e59d2dc 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -18,6 +18,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; extern PGDLLIMPORT bool session_variables_ambiguity_warning; +extern PGDLLIMPORT bool session_variables_use_fence_warning_guard; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 3e6b73684fd..66f1959a329 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1863,3 +1863,59 @@ SELECT tv; (1 row) DROP VARIABLE tv; +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; +CREATE VARIABLE a AS int; +LET a = 10; +CREATE TABLE test_table(a int, b int); +INSERT INTO test_table VALUES(20, 20); +-- no warning +SELECT a; + a +---- + 10 +(1 row) + +-- warning variable is shadowed +SELECT a, b FROM test_table; +WARNING: session variable "a" is shadowed +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a | b +----+---- + 20 | 20 +(1 row) + +-- no warning +SELECT variable(a) FROM test_table; + a +---- + 10 +(1 row) + +ALTER TABLE test_table DROP COLUMN a; +-- warning - variable fence is not used +SELECT a, b FROM test_table; +WARNING: session variable "a" is not used inside variable fence +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: The collision of session variable' names and column names is possible. + a | b +----+---- + 10 | 20 +(1 row) + +-- no warning +SELECT variable(a), b FROM test_table; + a | b +----+---- + 10 | 20 +(1 row) + +DROP VARIABLE a; +DROP TABLE test_table; +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 68fc0cde44a..45510e2ea54 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1266,3 +1266,38 @@ COMMIT; SELECT tv; DROP VARIABLE tv; + +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; + +CREATE VARIABLE a AS int; +LET a = 10; + +CREATE TABLE test_table(a int, b int); + +INSERT INTO test_table VALUES(20, 20); + +-- no warning +SELECT a; + +-- warning variable is shadowed +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a) FROM test_table; + +ALTER TABLE test_table DROP COLUMN a; + +-- warning - variable fence is not used +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a), b FROM test_table; + +DROP VARIABLE a; +DROP TABLE test_table; + +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; -- 2.47.0 [text/x-patch] v20241119-2-0022-set-verbosity-mode-in-regress-tests.patch (1.4K, 5-v20241119-2-0022-set-verbosity-mode-in-regress-tests.patch) download | inline diff: From a68fd2eee6823a9e5abd39f29876d67a2f4b8ae1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 19 Nov 2024 22:27:32 +0100 Subject: [PATCH 22/22] set verbosity mode in regress tests --- src/test/regress/expected/session_variables.out | 2 ++ src/test/regress/sql/session_variables.sql | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 66f1959a329..03ef5635f9c 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1889,12 +1889,14 @@ DETAIL: Session variables can be shadowed by columns, routine's variables and r (1 row) -- no warning +\set VERBOSITY verbose SELECT variable(a) FROM test_table; a ---- 10 (1 row) +\set VERBOSITY default ALTER TABLE test_table DROP COLUMN a; -- warning - variable fence is not used SELECT a, b FROM test_table; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 45510e2ea54..8089eae79b9 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1285,7 +1285,9 @@ SELECT a; SELECT a, b FROM test_table; -- no warning +\set VERBOSITY verbose SELECT variable(a) FROM test_table; +\set VERBOSITY default ALTER TABLE test_table DROP COLUMN a; -- 2.47.0 [text/x-patch] v20241119-2-0019-transactional-variables.patch (39.2K, 6-v20241119-2-0019-transactional-variables.patch) download | inline diff: From 4cf664b8b049d206479f4dd9650b123851b1dd9c Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:11:33 +0100 Subject: [PATCH 19/22] transactional variables This commit implements transactional variables. The content of transactional session variables is sensitive to transactions and subtransactions. Any transactional variable holds a history of values necessary for revert. This history is cleaned (purged) when a) we read the variable, b) when the transaction is finished. We don't try to purge, when the last modification of a variable is from current subtransaction, or when purge was processed in current subtransaction. This patch is based on my work from Feb 2020 and it is related to discussion about features related to session variables. Now, when the all features are separated to isolated patches I can revitalize this patch, because it doesn't increase complexity of basic patches. Unlike other patches, this patch is without any review at this moment (Feb 2024), but the feature should be fully functional for people who are interested about this feature (for testing). --- doc/src/sgml/catalogs.sgml | 12 + doc/src/sgml/ref/create_variable.sgml | 10 +- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 301 +++++++++++++++++- src/backend/parser/gram.y | 50 +-- src/bin/pg_dump/pg_dump.c | 10 +- src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 +++ src/bin/psql/describe.c | 4 +- src/bin/psql/tab-complete.in.c | 5 + src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/kwlist.h | 1 + src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 110 ++++++- src/test/regress/sql/session_variables.sql | 58 ++++ 16 files changed, 592 insertions(+), 50 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 9a8886fc69a..1064b3749e3 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9859,6 +9859,18 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry><structfield>varistransact</structfield></entry> + <entry><type>boolean</type></entry> + <entry></entry> + <entry> + True, when the variable is <quote>transactional</quote>. In the case + of a transaction rollback, transactional variables are reset to the + value they had when the transaction started. The default value is + <literal>false</literal>. + </entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>vareoxaction</structfield> <type>char</type> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 00938743c0d..b73f032ddbb 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] [ { TRANSACTIONAL | TRANSACTION } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> @@ -47,6 +47,14 @@ CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replac same as regular variables in procedural languages. </para> + <para> + When a schema variable is created with a + <command>CREATE TRANSACTIONAL VARIABLE</command> command, the variables + content changes are transactional: in case of rollback, they are reset to + their value at the beginning of the transaction or the latest subtransaction. + The variable content is only hold in memory, and thus is not persistent. + </para> + <para> Session variables are retrieved by the <command>SELECT</command> command. Their value is set with the <command>LET</command> command. diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index f261e3fd770..771c2816048 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -42,6 +42,7 @@ static ObjectAddress create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -59,6 +60,7 @@ create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -123,6 +125,7 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); + values[Anum_pg_variable_varistransact - 1] = BoolGetDatum(is_transact); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -267,6 +270,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) stmt->if_not_exists, stmt->not_null, stmt->is_immutable, + stmt->is_transact, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 7f34f93b542..978c84c9bdb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -47,6 +47,19 @@ typedef struct SVariableXActDropItem SubTransactionId deleting_subid; } SVariableXActDropItem; +/* + * Used for transactional variables. Holds prev version. + */ +typedef struct PrevValue +{ + Datum value; + bool isnull; + + SubTransactionId modify_subid; + + struct PrevValue *prev_value; +} PrevValue; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -68,6 +81,25 @@ typedef struct SVariableData bool isnull; Datum value; + /* + * We don't need stack versions modified in same subtransaction. + * Used by transactional variables only. The value of transactional + * variable can be returned immediately when modify_subid is same + * like current subid. + */ + SubTransactionId modify_subid; + + /* + * When the modify_subid is different than current subid, then + * we need to recheck versions and throw versions related to + * reverted transactions. When purge_subid is same like current subid + * we can return the value of transaction variable without this + * recheck. + */ + SubTransactionId purge_subid; + + PrevValue *prev_value; + Oid typid; int16 typlen; bool typbyval; @@ -86,6 +118,7 @@ typedef struct SVariableData bool not_null; bool is_immutable; + bool is_transact; bool reset_at_eox; @@ -125,6 +158,9 @@ static bool needs_validation = false; */ static bool has_session_variables_with_reset_at_eox = false; +/* true, when transactional variables was modified */ +static bool has_modified_transactional_variables = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -140,6 +176,7 @@ static List *xact_drop_items = NIL; static void register_session_variable_xact_drop(Oid varid); static void unregister_session_variable_xact_drop(Oid varid); +static bool purge_session_variable(SVariable svar); /* * Callback function for session variable invalidation. @@ -292,8 +329,25 @@ unregister_session_variable_xact_drop(Oid varid) * Release stored value, free memory */ static void -free_session_variable_value(SVariable svar) +free_session_variable_value(SVariable svar, bool deep_free) { + if (deep_free) + { + PrevValue *prev_value = svar->prev_value; + PrevValue *next_value; + + while (prev_value) + { + if (!svar->typbyval) + pfree(DatumGetPointer(prev_value->value)); + + next_value = prev_value->prev_value; + pfree(prev_value); + + prev_value = next_value; + } + } + /* clean the current value */ if (!svar->isnull) { @@ -390,7 +444,7 @@ remove_invalid_session_variables(bool atEOX) { Oid varid = svar->varid; - free_session_variable_value(svar); + free_session_variable_value(svar, true); hash_search(sessionvars, &varid, HASH_REMOVE, NULL); svar = NULL; } @@ -426,6 +480,94 @@ remove_session_variables_with_reset_at_eox(void) has_session_variables_with_reset_at_eox = false; } +/* + * remove prev values at eox + */ +static void +remove_prev_values_at_eox(bool isCommit) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_modified_transactional_variables) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->is_transact && svar->modify_subid != InvalidSubTransactionId) + { + if (isCommit) + { + if (purge_session_variable(svar)) + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + else + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId) + break; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + if (prev_value) + { + svar->value = prev_value->value; + svar->isnull = prev_value->isnull; + + pfree(prev_value); + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + + /* when svar is still valid (not removed from sessionvars */ + if (svar) + { + svar->modify_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + } + } + } + + has_modified_transactional_variables = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -473,6 +615,8 @@ AtPreEOXact_SessionVariables(bool isCommit) remove_invalid_session_variables(true); } + remove_prev_values_at_eox(isCommit); + /* * We have to clean xact_drop_items. All related variables are dropped * now, or lost inside aborted transaction. @@ -618,6 +762,7 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->not_null = varform->varnotnull; svar->is_immutable = varform->varisimmutable; + svar->is_transact = varform->varistransact; svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; @@ -643,6 +788,17 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->isnull = true; svar->value = (Datum) 0; + if (svar->is_transact) + { + svar->modify_subid = GetCurrentSubTransactionId(); + has_modified_transactional_variables = true; + } + else + svar->modify_subid = InvalidSubTransactionId; + + svar->purge_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + svar->is_valid = true; svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, @@ -676,6 +832,69 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) ReleaseSysCache(tup); } +/* + * Try to remove all previous versions related to reverted transactions. + * Returns true, when valid version was found. + */ +static bool +purge_session_variable(SVariable svar) +{ + SubTransactionId current_subid; + PrevValue *prev_value; + bool found = true; + + Assert(svar->is_transact); + + if (svar->modify_subid == InvalidSubTransactionId) + return true; + + current_subid = GetCurrentSubTransactionId(); + + if (svar->modify_subid == current_subid) + return true; + + if (svar->purge_subid == current_subid) + return true; + + if (SubTransactionIsActive(svar->modify_subid)) + { + svar->purge_subid = current_subid; + return true; + } + + prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId || + SubTransactionIsActive(current_pv->modify_subid)) + { + svar->value = current_pv->value; + svar->isnull = current_pv->isnull; + svar->modify_subid = current_pv->modify_subid; + + prev_value = current_pv->prev_value; + pfree(current_pv); + + found = true; + break; + } + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + svar->prev_value = prev_value; + svar->purge_subid = current_subid; + + return found; +} + /* * Assign a new value to the session variable. It is copied to * SVariableMemoryContext if necessary. @@ -688,6 +907,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Datum newval; SVariableData locsvar, *_svar; + SubTransactionId current_subid = GetCurrentSubTransactionId(); + SubTransactionId prev_purge_subid = InvalidSubTransactionId; + bool save_prev_value; + PrevValue *prev_value; Assert(svar); Assert(!isnull || value == (Datum) 0); @@ -723,6 +946,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else _svar = svar; + if (_svar->is_transact && _svar->create_lsn == svar->create_lsn) + { + Assert(svar->typid == _svar->typid); + Assert(svar->typbyval == _svar->typbyval); + Assert(svar->typlen == _svar->typlen); + + save_prev_value = svar->modify_subid != current_subid; + prev_value = svar->prev_value; + } + else + { + save_prev_value = false; + prev_value = NULL; + } + if (!isnull) { MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); @@ -734,7 +972,37 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else newval = value; - free_session_variable_value(svar); + if (save_prev_value) + { + volatile PrevValue *new_prev_value; + + PG_TRY(); + { + new_prev_value = MemoryContextAlloc(SVariableMemoryContext, + sizeof(PrevValue)); + } + PG_CATCH(); + { + /* release mem from persistent content */ + if (newval != value) + pfree(DatumGetPointer(newval)); + PG_RE_THROW(); + } + PG_END_TRY(); + + new_prev_value->value = svar->value; + new_prev_value->isnull = svar->isnull; + new_prev_value->modify_subid = svar->modify_subid; + new_prev_value->prev_value = prev_value; + + prev_value = (PrevValue *) new_prev_value; + prev_purge_subid = svar->purge_subid; + + has_modified_transactional_variables = true; + + } + else + free_session_variable_value(svar, prev_value == NULL); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", get_namespace_name(get_session_variable_namespace(svar->varid)), @@ -747,6 +1015,9 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + svar->modify_subid = current_subid; + svar->purge_subid = prev_purge_subid; + svar->prev_value = prev_value; /* don't allow more changes of value when variable is IMMUTABLE */ if (svar->is_immutable) @@ -848,6 +1119,8 @@ get_session_variable(Oid varid) else svar->is_valid = false; +reinit: + /* * Force setup for not yet initialized variables or variables that cannot * be validated. @@ -880,6 +1153,28 @@ get_session_variable(Oid varid) varid); } + /* + * Transactional variables should be purged before (remove + * versions created by possibly reverted subtransactions). + */ + if (svar->is_transact && + svar->modify_subid != GetCurrentSubTransactionId() && + svar->modify_subid != InvalidSubTransactionId) + { + if (!purge_session_variable(svar)) + { + /* force reinit */ + svar->is_valid = false; + + /* + * In next iteration modify_subid should be + * InvalidSubTransactionId or current subid, + * so there is not risk of infinity cycle. + */ + goto reinit; + } + } + /* ensure the returned data is still of the correct domain */ if (svar->is_domain) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index bc8e25b1933..af9d640c0e2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,7 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt -%type <boolean> OptNotNull OptImmutable +%type <boolean> OptNotNull OptImmutable OptTransactional /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN - TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM + TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSACTIONAL TRANSFORM TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P @@ -5240,31 +5240,33 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptTransactional OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $5->relpersistence = $2; - n->is_immutable = $3; - n->variable = $5; - n->typeName = $7; - n->collClause = (CollateClause *) $8; - n->not_null = $9; - n->defexpr = $10; - n->XactEndAction = $11; + $6->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->not_null = $10; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptTransactional OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $8->relpersistence = $2; - n->is_immutable = $3; - n->variable = $8; - n->typeName = $10; - n->collClause = (CollateClause *) $11; - n->not_null = $12; - n->defexpr = $13; - n->XactEndAction = $14; + $9->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $9; + n->typeName = $11; + n->collClause = (CollateClause *) $12; + n->not_null = $13; + n->defexpr = $14; + n->XactEndAction = $15; n->if_not_exists = true; $$ = (Node *) n; } @@ -5292,6 +5294,12 @@ OptImmutable: IMMUTABLE { $$ = true; } | /* EMPTY */ { $$ = false; } ; +OptTransactional: + TRANSACTION { $$ = true; } + | TRANSACTIONAL { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -18084,6 +18092,7 @@ unreserved_keyword: | TEXT_P | TIES | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TRIGGER | TRUNCATE @@ -18732,6 +18741,7 @@ bare_label_keyword: | TIMESTAMP | TRAILING | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TREAT | TRIGGER diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 59f4103ca4f..762cd020ea9 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5439,6 +5439,7 @@ getVariables(Archive *fout) int i_varcollation; int i_varnotnull; int i_varisimmutable; + int i_varistransact; int i_varacl; int i_acldefault; int i, @@ -5463,6 +5464,7 @@ getVariables(Archive *fout) " END AS varcollation,\n" " v.varnotnull,\n" " v.varisimmutable,\n" + " v.varistransact,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5485,6 +5487,7 @@ getVariables(Archive *fout) i_varcollation = PQfnumber(res, "varcollation"); i_varnotnull = PQfnumber(res, "varnotnull"); i_varisimmutable = PQfnumber(res, "varisimmutable"); + i_varistransact = PQfnumber(res, "varistransact"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5513,6 +5516,7 @@ getVariables(Archive *fout) varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; + varinfo[i].varistransact = *(PQgetvalue(res, i, i_varistransact)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5560,6 +5564,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vardefexpr; const char *varxactendaction; const char *varisimmutable; + const char *varistransact; Oid varcollation; bool varnotnull; @@ -5577,12 +5582,13 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) varcollation = varinfo->varcollation; varnotnull = varinfo->varnotnull; varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; + varistransact = varinfo->varistransact ? "TRANSACTIONAL " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", - varisimmutable, qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %s%sVARIABLE %s AS %s", + varistransact, varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f7921f942d0..310701f09e1 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -719,6 +719,7 @@ typedef struct _VariableInfo const char *rolname; /* name of owner, or empty string */ bool varnotnull; bool varisimmutable; + bool varistransact; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 47cb59356e0..b39bddd3e40 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4077,6 +4077,42 @@ my %tests = ( }, }, + 'CREATE TRANSACTIONAL VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index cc69f07f1f9..8862539d76e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " NOT v.varnotnull as \"%s\",\n" " NOT v.varisimmutable as \"%s\",\n" + " v.varistransact as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5241,6 +5242,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Owner"), gettext_noop("Nullable"), gettext_noop("Mutable"), + gettext_noop("Transactional"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b653dda1fe8..e955522d2cb 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1305,6 +1305,8 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"TEXT SEARCH", NULL, NULL, NULL}, {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER}, + {"TRANSACTIONAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TRANSACTIONAL + * VARIABLE ... */ {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"}, {"TYPE", NULL, NULL, &Query_for_list_of_datatypes}, {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE @@ -3947,8 +3949,11 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ + else if (Matches("CREATE", "TRANSACTION|TRANSACTIONAL")) + COMPLETE_WITH("IMMUTABLE", "VARIABLE"); else if (TailMatches("CREATE", "VARIABLE", MatchAny) || TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("TRANSACTION|TRANSACTIONAL", "VARIABLE", MatchAny) || TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 68bc49a0e27..1dc44edf6e5 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -61,6 +61,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* don't allow changes */ bool varisimmutable BKI_DEFAULT(f); + /* supports transactions */ + bool varistransact BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bf820828dd9..a4372f184df 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3481,6 +3481,7 @@ typedef struct CreateSessionVarStmt bool if_not_exists; /* do nothing if it already exists */ bool not_null; /* disallow nulls */ bool is_immutable; /* don't allow changes */ + bool is_transact; /* supports transactions */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index aefe51f335b..125eb819b76 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -455,6 +455,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("to", TO, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("transactional", TRANSACTIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index e1635f686c2..da9f4a7030a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | t | t | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 5a66fc9ffab..3e6b73684fd 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1763,3 +1763,103 @@ SELECT var1; LET var1 = 30; ERROR: session variable "public.var1" is declared IMMUTABLE DROP VARIABLE var1; +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; +BEGIN; + LET tv = 100; + SELECT tv; + tv +----- + 100 +(1 row) + +ROLLBACK; +SELECT tv; + tv +---- + 0 +(1 row) + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 0; + SELECT tv; + tv +---- + 0 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +-- test subtransactions +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +COMMIT; +SELECT tv; + tv +---- + 1 +(1 row) + +DROP VARIABLE tv; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index fcd783e966c..68fc0cde44a 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1208,3 +1208,61 @@ SELECT var1; LET var1 = 30; DROP VARIABLE var1; + +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; + +BEGIN; + LET tv = 100; + SELECT tv; +ROLLBACK; + +SELECT tv; + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; + +SELECT tv; + +BEGIN; + LET tv = 0; + SELECT tv; +ROLLBACK; + +SELECT tv; + +-- test subtransactions + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +ROLLBACK; + +SELECT tv; + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +COMMIT; + +SELECT tv; + +DROP VARIABLE tv; -- 2.47.0 [text/x-patch] v20241119-2-0018-this-patch-changes-error-message-column-doesn-t-exis.patch (29.2K, 7-v20241119-2-0018-this-patch-changes-error-message-column-doesn-t-exis.patch) download | inline diff: From 9925a2ead22cf0b40f547ee665593bf4325ccf16 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:10:39 +0100 Subject: [PATCH 18/22] this patch changes error message "column doesn't exist" to message "column or variable doesn't exist" The error message will be more correct. Today, missing PL/pgSQL variable can be reported. The change has impact on lot of regress tests not directly related to session variables, and then it is distributed as separate patch --- src/backend/parser/parse_expr.c | 2 +- src/backend/parser/parse_relation.c | 24 +++++++++++--- src/backend/parser/parse_target.c | 8 +++-- src/include/parser/parse_expr.h | 2 ++ src/pl/plpgsql/src/expected/plpgsql_array.out | 2 +- .../plpgsql/src/expected/plpgsql_record.out | 4 +-- .../src/expected/plpgsql_session_variable.out | 2 +- src/pl/tcl/expected/pltcl_queries.out | 12 +++---- .../isolation/expected/session-variable.out | 4 +-- src/test/regress/expected/alter_table.out | 32 +++++++++---------- src/test/regress/expected/copy2.out | 2 +- src/test/regress/expected/errors.out | 8 ++--- src/test/regress/expected/join.out | 12 +++---- src/test/regress/expected/namespace.out | 2 +- src/test/regress/expected/numerology.out | 2 +- src/test/regress/expected/plpgsql.out | 12 +++---- src/test/regress/expected/psql.out | 2 +- src/test/regress/expected/rules.out | 2 +- .../regress/expected/session_variables.out | 6 ++-- src/test/regress/expected/transactions.out | 4 +-- src/test/regress/expected/union.out | 2 +- 21 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index bcbb1f564af..16bf52c9854 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -520,7 +520,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) * printed. When we are in an expression where session variables cannot be * used, we raise the first form of error message. */ -static bool +bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind) { bool result = false; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 95ae2108044..fa9a30bd138 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -27,6 +27,7 @@ #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_enr.h" +#include "parser/parse_expr.h" #include "parser/parse_relation.h" #include "parser/parse_type.h" #include "parser/parsetree.h" @@ -3730,6 +3731,19 @@ errorMissingRTE(ParseState *pstate, RangeVar *relation) parser_errposition(pstate, relation->location))); } +/* + * set message "column does not exist" or "column or variable does not exist" + * in dependency if expression context allows session variables. + */ +static int +column_or_variable_does_not_exists(ParseState *pstate, const char *colname) +{ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + return errmsg("column or variable \"%s\" does not exist", colname); + else + return errmsg("column \"%s\" does not exist", colname); +} + /* * Generate a suitable error about a missing column. * @@ -3764,7 +3778,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There are columns named \"%s\", but they are in tables that cannot be referenced from this part of the query.", colname), !relname ? errhint("Try using a table-qualified name.") : 0, @@ -3774,7 +3788,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There is a column named \"%s\" in table \"%s\", but it cannot be referenced from this part of the query.", colname, state->rexact1->eref->aliasname), rte_visible_if_lateral(pstate, state->rexact1) ? @@ -3792,14 +3806,14 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), parser_errposition(pstate, location))); /* Handle case where we have a single alternative spelling to offer */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, @@ -3813,7 +3827,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 76bf88c3ca2..6af6b801ad4 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -779,7 +779,9 @@ transformAssignmentIndirection(ParseState *pstate, if (!typrelid) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because its type %s is not a composite type" : + "cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); @@ -788,7 +790,9 @@ transformAssignmentIndirection(ParseState *pstate, if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because there is no such column in data type %s" : + "cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 0d64b300f8a..e5aebf99b4a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -23,4 +23,6 @@ extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKin extern const char *ParseExprKindName(ParseExprKind exprKind); +extern bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind); + #endif /* PARSE_EXPR_H */ diff --git a/src/pl/plpgsql/src/expected/plpgsql_array.out b/src/pl/plpgsql/src/expected/plpgsql_array.out index ad60e0e8be3..c1ab9fd7ed9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_array.out +++ b/src/pl/plpgsql/src/expected/plpgsql_array.out @@ -41,7 +41,7 @@ NOTICE: a = {"(,11)"}, a[1].i = 11 -- perhaps this ought to work, but for now it doesn't: do $$ declare a complex[]; begin a[1:2].i := array[11,12]; raise notice 'a = %', a; end$$; -ERROR: cannot assign to field "i" of column "a" because its type complex[] is not a composite type +ERROR: cannot assign to field "i" of column or variable "a" because its type complex[] is not a composite type LINE 1: a[1:2].i := array[11,12] ^ QUERY: a[1:2].i := array[11,12] diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index 6974c8f4a44..3970ac721c9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -135,7 +135,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ declare c nested_int8s; begin c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "c" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "c" because there is no such column in data type two_int8s LINE 1: c.c2.x = 1 ^ QUERY: c.c2.x = 1 @@ -157,7 +157,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "b.c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ <<b>> declare c nested_int8s; begin b.c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "b" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "b" because there is no such column in data type two_int8s LINE 1: b.c.c2.x = 1 ^ QUERY: b.c.c2.x = 1 diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ecac64fcfb4..8bd2b398bdd 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -232,7 +232,7 @@ SET ROLE TO DEFAULT; SET ROLE TO regress_var_exec_role; -- should fail, but not crash SELECT var_read_func(); -ERROR: column "plpgsql_sv_var1" does not exist +ERROR: column or variable "plpgsql_sv_var1" does not exist LINE 1: plpgsql_sv_var1 ^ QUERY: plpgsql_sv_var1 diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out index 35cc6e62aad..497cd38189b 100644 --- a/src/pl/tcl/expected/pltcl_queries.out +++ b/src/pl/tcl/expected/pltcl_queries.out @@ -450,12 +450,12 @@ CONTEXT: while executing "__PLTcl_proc_tcl_eval_text spi_prepare\ a\ \"b\ \{\"" in PL/Tcl function tcl_eval(text) select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$); - tcl_error_handling_test --------------------------------------- - SQLSTATE: 42703 + - condition: undefined_column + - cursor_position: 8 + - message: column "moo" does not exist+ + tcl_error_handling_test +-------------------------------------------------- + SQLSTATE: 42703 + + condition: undefined_column + + cursor_position: 8 + + message: column or variable "moo" does not exist+ statement: select moo (1 row) diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index e9a254bcd12..d8848d555cd 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -10,7 +10,7 @@ test step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist starting permutation: let val s1 drop val sr1 step let: LET myvar = 'test'; @@ -23,7 +23,7 @@ test step s1: BEGIN; step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist step sr1: ROLLBACK; starting permutation: let val dbg drop create dbg val diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 2212c8dbb59..35b3a3bd302 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1295,19 +1295,19 @@ select * from atacc1; (1 row) select * from atacc1 order by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 order by a; ^ select * from atacc1 order by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 order by "........pg.dropped.1........"... ^ select * from atacc1 group by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 group by a; ^ select * from atacc1 group by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 group by "........pg.dropped.1........"... ^ select atacc1.* from atacc1; @@ -1317,7 +1317,7 @@ select atacc1.* from atacc1; (1 row) select a from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a from atacc1; ^ select atacc1.a from atacc1; @@ -1331,15 +1331,15 @@ select b,c,d from atacc1; (1 row) select a,b,c,d from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a,b,c,d from atacc1; ^ select * from atacc1 where a = 1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 where a = 1; ^ select "........pg.dropped.1........" from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........" from atacc1; ^ select atacc1."........pg.dropped.1........" from atacc1; @@ -1347,11 +1347,11 @@ ERROR: column atacc1.........pg.dropped.1........ does not exist LINE 1: select atacc1."........pg.dropped.1........" from atacc1; ^ select "........pg.dropped.1........",b,c,d from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........",b,c,d from atacc1; ^ select * from atacc1 where "........pg.dropped.1........" = 1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 where "........pg.dropped.1........" = ... ^ -- UPDATEs @@ -1360,7 +1360,7 @@ ERROR: column "a" of relation "atacc1" does not exist LINE 1: update atacc1 set a = 3; ^ update atacc1 set b = 2 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: update atacc1 set b = 2 where a = 3; ^ update atacc1 set "........pg.dropped.1........" = 3; @@ -1368,7 +1368,7 @@ ERROR: column "........pg.dropped.1........" of relation "atacc1" does not exis LINE 1: update atacc1 set "........pg.dropped.1........" = 3; ^ update atacc1 set b = 2 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: update atacc1 set b = 2 where "........pg.dropped.1........"... ^ -- INSERTs @@ -1416,11 +1416,11 @@ LINE 1: insert into atacc1 ("........pg.dropped.1........",b,c,d) va... ^ -- DELETEs delete from atacc1 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: delete from atacc1 where a = 3; ^ delete from atacc1 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: delete from atacc1 where "........pg.dropped.1........" = 3; ^ delete from atacc1; @@ -1706,7 +1706,7 @@ select f1 from c1; alter table c1 drop column f1; select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". @@ -1720,7 +1720,7 @@ ERROR: cannot drop inherited column "f1" alter table p1 drop column f1; -- c1.f1 is dropped now, since there is no local definition for it select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 64ea33aeae8..6874f5ae0d1 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -160,7 +160,7 @@ LINE 1: COPY x TO stdout WHERE a = 1; COPY x from stdin WHERE a = 50004; COPY x from stdin WHERE a > 60003; COPY x from stdin WHERE f > 60003; -ERROR: column "f" does not exist +ERROR: column or variable "f" does not exist LINE 1: COPY x from stdin WHERE f > 60003; ^ COPY x from stdin WHERE a = max(x.b); diff --git a/src/test/regress/expected/errors.out b/src/test/regress/expected/errors.out index 8c527474daf..e53ae451dfc 100644 --- a/src/test/regress/expected/errors.out +++ b/src/test/regress/expected/errors.out @@ -27,7 +27,7 @@ LINE 1: select * from nonesuch; ^ -- bad name in target list select nonesuch from pg_database; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select nonesuch from pg_database; ^ -- empty distinct list isn't OK @@ -37,17 +37,17 @@ LINE 1: select distinct from pg_database; ^ -- bad attribute name on lhs of operator select * from pg_database where nonesuch = pg_database.datname; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select * from pg_database where nonesuch = pg_database.datna... ^ -- bad attribute name on rhs of operator select * from pg_database where pg_database.datname = nonesuch; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: ...ect * from pg_database where pg_database.datname = nonesuch; ^ -- bad attribute name in select distinct on select distinct on (foobar) * from pg_database; -ERROR: column "foobar" does not exist +ERROR: column or variable "foobar" does not exist LINE 1: select distinct on (foobar) * from pg_database; ^ -- grouping with FOR UPDATE diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 9b2973694ff..d9f16aeb978 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6310,13 +6310,13 @@ LINE 1: select t2.uunique1 from HINT: Perhaps you meant to reference the column "t2.unique1". select uunique1 from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once -ERROR: column "uunique1" does not exist +ERROR: column or variable "uunique1" does not exist LINE 1: select uunique1 from ^ HINT: Perhaps you meant to reference the column "t1.unique1" or the column "t2.unique1". select ctid from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, need qualification -ERROR: column "ctid" does not exist +ERROR: column or variable "ctid" does not exist LINE 1: select ctid from ^ DETAIL: There are columns named "ctid", but they are in tables that cannot be referenced from this part of the query. @@ -7413,7 +7413,7 @@ lateral (select * from int8_tbl t1, -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7425,7 +7425,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss; DETAIL: There is an entry for table "a", but it cannot be referenced from this part of the query. HINT: To reference that table, you must mark this subquery with LATERAL. select f1,g from int4_tbl a cross join (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7462,7 +7462,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i... create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl; -- error, can't do this: update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. @@ -7482,7 +7482,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1) ERROR: table name "xx1" specified more than once -- also errors: delete from xx1 using (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/namespace.out b/src/test/regress/expected/namespace.out index dbbda72d395..d98793f92a7 100644 --- a/src/test/regress/expected/namespace.out +++ b/src/test/regress/expected/namespace.out @@ -23,7 +23,7 @@ BEGIN; SET search_path to public, test_ns_schema_1; CREATE SCHEMA test_ns_schema_2 CREATE VIEW abc_view AS SELECT c FROM abc; -ERROR: column "c" does not exist +ERROR: column or variable "c" does not exist LINE 2: CREATE VIEW abc_view AS SELECT c FROM abc; ^ COMMIT; diff --git a/src/test/regress/expected/numerology.out b/src/test/regress/expected/numerology.out index 9e23166fed1..672012e1461 100644 --- a/src/test/regress/expected/numerology.out +++ b/src/test/regress/expected/numerology.out @@ -314,7 +314,7 @@ NOTICE: i = 1002 NOTICE: i = 1003 -- error cases SELECT _100; -ERROR: column "_100" does not exist +ERROR: column or variable "_100" does not exist LINE 1: SELECT _100; ^ SELECT 100_; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 0a6945581bd..5c599ba312e 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -2598,7 +2598,7 @@ end; $$ language plpgsql; -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION -- blocks select excpt_test1(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -2613,7 +2613,7 @@ begin end; $$ language plpgsql; -- should fail select excpt_test2(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -4675,7 +4675,7 @@ BEGIN RAISE NOTICE '%, %', r.roomno, r.comment; END LOOP; END$$; -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn... ^ QUERY: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno @@ -4717,7 +4717,7 @@ begin raise notice 'x = %', x; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -4729,7 +4729,7 @@ begin raise notice 'x = %, y = %', x, y; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -5769,7 +5769,7 @@ ALTER TABLE alter_table_under_transition_tables DROP column name; UPDATE alter_table_under_transition_tables SET id = id; -ERROR: column "name" does not exist +ERROR: column or variable "name" does not exist LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d) ^ QUERY: (SELECT string_agg(id || '=' || name, ',') FROM d) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 77a7bf45c8a..e1635f686c2 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -214,7 +214,7 @@ select $1::int as col \bind 1 \g \bind 2 \g -- errors -- parse error SELECT foo \bind \g -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT foo ^ -- tcop error diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 3014d047fef..7378f95b17d 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo; -- this should fail because f1 is not exposed for unqualified reference: create rule rules_foorule as on insert to rules_foo where f1 < 100 do instead insert into rules_foo2 values (f1); -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 2: do instead insert into rules_foo2 values (f1); ^ DETAIL: There are columns named "f1", but they are in tables that cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 96283ed0b9a..5a66fc9ffab 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -310,7 +310,7 @@ ERROR: session variable "var1" doesn't exist LINE 1: LET var1 = pi(); ^ SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ -- should be ok @@ -522,7 +522,7 @@ SELECT v1; -- should fail, attribute doesn't exist LET v1.x = 10; -ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +ERROR: cannot assign to field "x" of column or variable "v1" because there is no such column in data type t1 LINE 1: LET v1.x = 10; ^ -- should fail, don't allow multi column query @@ -1070,7 +1070,7 @@ BEGIN; DROP VARIABLE var1; -- should fail SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ ROLLBACK; diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out index 7f5757e89c4..b05b16d94bf 100644 --- a/src/test/regress/expected/transactions.out +++ b/src/test/regress/expected/transactions.out @@ -256,7 +256,7 @@ SELECT * FROM trans_barbaz; -- should have 1 BEGIN; SAVEPOINT one; SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ ROLLBACK TO SAVEPOINT one; @@ -305,7 +305,7 @@ BEGIN; SAVEPOINT one; INSERT INTO savepoints VALUES (5); SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ COMMIT; diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index c73631a9a1d..5c0eacc5f15 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -922,7 +922,7 @@ ORDER BY q2,q1; -- This should fail, because q2 isn't a name of an EXCEPT output column SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1; -ERROR: column "q2" does not exist +ERROR: column or variable "q2" does not exist LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1... ^ DETAIL: There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query. -- 2.47.0 [text/x-patch] v20241119-2-0017-expression-with-session-variables-can-be-inlined.patch (4.2K, 8-v20241119-2-0017-expression-with-session-variables-can-be-inlined.patch) download | inline diff: From b86f079f9ce82f62c7e92eab05c32c673c883b6d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 20:35:38 +0100 Subject: [PATCH 17/22] expression with session variables can be inlined There is not an reason why session variables should to block inlining. (of SQL functions). I can imagine some use cases like wrapping, and inlining significantly reduces an overhead of SQL functions. --- src/backend/optimizer/util/clauses.c | 37 ++++++++++++++----- .../regress/expected/session_variables.out | 7 ++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8cee732561d..f79937980ee 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -4744,8 +4744,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - (list_length(querytree->targetList) != 1) || - querytree->hasSessionVariables) + (list_length(querytree->targetList) != 1)) goto fail; /* If the function result is composite, resolve it */ @@ -4949,21 +4948,39 @@ substitute_actual_parameters_mutator(Node *node, { if (node == NULL) return NULL; + + + /* + * SQL functions can contain two different kind of params. The nodes with + * paramkind PARAM_EXTERN are related to function's arguments (and should + * be replaced in this step), because this is how we apply the function's + * arguments for an expression. + * + * The nodes with paramkind PARAM_VARIABLE are related to usage of session + * variables. The values of session variables are not passed to expression + * by expression arguments, so it should not be replaced here by + * function's arguments. + */ if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind != PARAM_EXTERN) + if (param->paramkind != PARAM_EXTERN && + param->paramkind != PARAM_VARIABLE) elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind); - if (param->paramid <= 0 || param->paramid > context->nargs) - elog(ERROR, "invalid paramid: %d", param->paramid); - /* Count usage of parameter */ - context->usecounts[param->paramid - 1]++; + if (param->paramkind == PARAM_EXTERN) + { + if (param->paramid <= 0 || param->paramid > context->nargs) + elog(ERROR, "invalid paramid: %d", param->paramid); - /* Select the appropriate actual arg and replace the Param with it */ - /* We don't need to copy at this time (it'll get done later) */ - return list_nth(context->args, param->paramid - 1); + /* Count usage of parameter */ + context->usecounts[param->paramid - 1]++; + + /* Select the appropriate actual arg and replace the Param with it */ + /* We don't need to copy at this time (it'll get done later) */ + return list_nth(context->args, param->paramid - 1); + } } return expression_tree_mutator(node, substitute_actual_parameters_mutator, (void *) context); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 8df3a91c05e..96283ed0b9a 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -105,7 +105,6 @@ SELECT var1; ERROR: permission denied for session variable var1 SELECT sqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN @@ -212,10 +211,10 @@ SELECT sqlfx1(sqlfx2('Hello')); -- inlining is blocked EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); - QUERY PLAN ------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------- Result - Output: sqlfx1(sqlfx2('Hello'::character varying)) + Output: ((var1 || ', '::text) || ((var2 || ', '::text) || 'Hello'::text)) (2 rows) DROP FUNCTION sqlfx1(varchar); -- 2.47.0 [text/x-patch] v20241119-2-0016-plpgsql-implementation-for-LET-statement.patch (14.2K, 9-v20241119-2-0016-plpgsql-implementation-for-LET-statement.patch) download | inline diff: From 6abaec095d372050fd27fb91ab5afb2fd1519125 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 08:56:17 +0100 Subject: [PATCH 16/22] plpgsql implementation for LET statement PLpgSQL allows to call expression executor directly (for simple expression). This possibility strongly reduces overhead related to execution. Reimplementation of LET statement (inside PLpgSQL) allows to use this possibility, and strongly increase performance: CREATE VARIABLE svar int; DO $$ BEGIN FOR i IN 1..10000 LOOP LET svar = i; END LOOP; END; $$; From 120ms to 8ms (with assertions) (this is best case, but without it, the LET statement can be bottle neck). An alternative can be reimplementation of LET statement inside expression executor, and then SQL LET command can be executed in simple expression execution, but this increase an complexity of executor, but still the benefits is only inside plpgsql, so it is better to this optimization inside plpgsql. --- src/backend/executor/spi.c | 3 ++ src/backend/parser/analyze.c | 12 +++++ src/backend/parser/gram.y | 8 ++++ src/backend/parser/parser.c | 1 + src/include/nodes/parsenodes.h | 2 + src/include/parser/parser.h | 4 ++ src/pl/plpgsql/src/pl_exec.c | 55 +++++++++++++++++++++++ src/pl/plpgsql/src/pl_funcs.c | 24 ++++++++++ src/pl/plpgsql/src/pl_gram.y | 28 +++++++++++- src/pl/plpgsql/src/pl_reserved_kwlist.h | 1 + src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 + src/pl/plpgsql/src/plpgsql.h | 12 +++++ src/tools/pgindent/typedefs.list | 1 + 13 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 2fb2e73604e..5ae25d07ad7 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2991,6 +2991,9 @@ _SPI_error_callback(void *arg) case RAW_PARSE_PLPGSQL_ASSIGN3: errcontext("PL/pgSQL assignment \"%s\"", query); break; + case RAW_PARSE_PLPGSQL_LET: + errcontext("LET statement \"%s\"", query); + break; default: errcontext("SQL statement \"%s\"", query); break; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 496d75a494a..2edc51cfee9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -2014,6 +2014,18 @@ transformLetStmt(ParseState *pstate, LetStmt *stmt) stmt->query = (Node *) query; + /* + * Inside PL/pgSQL we don't want to execute LET statement as utility + * command, because it disallow to execute expression as simple + * expression. So for PL/pgSQL we have extra path, and we return SELECT. + * Then it can be executed by exec_eval_expr. Result is dirrectly assigned + * to target session variable inside PL/pgSQL LET statement handler. This + * is extra code, extra path, but possibility to get faster execution is + * too attractive. + */ + if (stmt->plpgsql_mode) + return query; + /* represent the command as a utility Query */ result = makeNode(Query); result->commandType = CMD_UTILITY; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6aea3bd35b2..bc8e25b1933 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -817,6 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %token MODE_PLPGSQL_ASSIGN1 %token MODE_PLPGSQL_ASSIGN2 %token MODE_PLPGSQL_ASSIGN3 +%token MODE_PLPGSQL_LET /* Precedence: lowest to highest */ @@ -948,6 +949,13 @@ parse_toplevel: pg_yyget_extra(yyscanner)->parsetree = list_make1(makeRawStmt((Node *) n, @2)); } + | MODE_PLPGSQL_LET LetStmt + { + LetStmt *n = (LetStmt *) $2; + n->plpgsql_mode = true; + pg_yyget_extra(yyscanner)->parsetree = + list_make1(makeRawStmt((Node *) n, 0)); + } ; /* diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c index 118488c3f30..f9430a32919 100644 --- a/src/backend/parser/parser.c +++ b/src/backend/parser/parser.c @@ -62,6 +62,7 @@ raw_parser(const char *str, RawParseMode mode) [RAW_PARSE_PLPGSQL_ASSIGN1] = MODE_PLPGSQL_ASSIGN1, [RAW_PARSE_PLPGSQL_ASSIGN2] = MODE_PLPGSQL_ASSIGN2, [RAW_PARSE_PLPGSQL_ASSIGN3] = MODE_PLPGSQL_ASSIGN3, + [RAW_PARSE_PLPGSQL_LET] = MODE_PLPGSQL_LET, }; yyextra.have_lookahead = true; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7308c323574..bf820828dd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2114,6 +2114,8 @@ typedef struct LetStmt NodeTag type; List *target; /* target variable */ Node *query; /* source expression */ + bool plpgsql_mode; /* true, when command will be executed + * (parsed) by plpgsql runtime */ int location; } LetStmt; diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h index be184ec5066..efbc76f0ad4 100644 --- a/src/include/parser/parser.h +++ b/src/include/parser/parser.h @@ -33,6 +33,9 @@ * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement, * and return a one-element List containing a RawStmt node. "n" * gives the number of dotted names comprising the target ColumnRef. + * + * RAW_PARSE_PLPGSQL_LET: parse a LET statement, and return a + * one-element List containing a RawStmt node. */ typedef enum { @@ -42,6 +45,7 @@ typedef enum RAW_PARSE_PLPGSQL_ASSIGN1, RAW_PARSE_PLPGSQL_ASSIGN2, RAW_PARSE_PLPGSQL_ASSIGN3, + RAW_PARSE_PLPGSQL_LET, } RawParseMode; /* Values for the backslash_quote GUC */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..b5cb1bee223 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -22,6 +22,7 @@ #include "access/tupconvert.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/spi.h" #include "executor/tstoreReceiver.h" @@ -323,6 +324,8 @@ static int exec_stmt_commit(PLpgSQL_execstate *estate, PLpgSQL_stmt_commit *stmt); static int exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt); +static int exec_stmt_let(PLpgSQL_execstate *estate, + PLpgSQL_stmt_let *let); static void plpgsql_estate_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, @@ -2116,6 +2119,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts) rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt); + break; + default: /* point err_stmt to parent, since this one seems corrupt */ estate->err_stmt = save_estmt; @@ -4999,6 +5006,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt) return PLPGSQL_RC_OK; } +/* ---------- + * exec_stmt_let Evaluate an expression and + * put the result into a session variable. + * ---------- + */ +static int +exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt) +{ + bool isNull; + Oid valtype; + int32 valtypmod; + Datum value; + Oid varid; + + List *plansources; + CachedPlanSource *plansource; + + value = exec_eval_expr(estate, + stmt->expr, + &isNull, + &valtype, + &valtypmod); + + /* + * Oid of target session variable is stored in Query structure. It is + * safer to read this value directly from the plan than to hold this value + * in the plpgsql context, because it's not necessary to handle + * invalidation of the cached value. Next operations are read only without + * any allocations, so we can expect that retrieving varid from Query + * should be fast. + */ + plansources = SPI_plan_get_plan_sources(stmt->expr->plan); + if (list_length(plansources) != 1) + elog(ERROR, "unexpected length of plansources of query for LET statement"); + + plansource = (CachedPlanSource *) linitial(plansources); + if (list_length(plansource->query_list) != 1) + elog(ERROR, "unexpected length of plansource of query for LET statement"); + + varid = linitial_node(Query, plansource->query_list)->resultVariable; + if (!OidIsValid(varid)) + elog(ERROR, "oid of target session variable is not valid"); + + SetSessionVariableWithSecurityCheck(varid, value, isNull); + + return PLPGSQL_RC_OK; +} + /* ---------- * exec_assign_expr Put an expression's result into a variable. * ---------- diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index eeb7c4d7c04..1b39351db7f 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt) return "COMMIT"; case PLPGSQL_STMT_ROLLBACK: return "ROLLBACK"; + case PLPGSQL_STMT_LET: + return "LET"; } return "unknown"; @@ -370,6 +372,7 @@ static void free_perform(PLpgSQL_stmt_perform *stmt); static void free_call(PLpgSQL_stmt_call *stmt); static void free_commit(PLpgSQL_stmt_commit *stmt); static void free_rollback(PLpgSQL_stmt_rollback *stmt); +static void free_let(PLpgSQL_stmt_let *stmt); static void free_expr(PLpgSQL_expr *expr); @@ -459,6 +462,9 @@ free_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: free_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + free_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -713,6 +719,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt) { } +static void +free_let(PLpgSQL_stmt_let *stmt) +{ + free_expr(stmt->expr); +} + static void free_expr(PLpgSQL_expr *expr) { @@ -815,6 +827,7 @@ static void dump_perform(PLpgSQL_stmt_perform *stmt); static void dump_call(PLpgSQL_stmt_call *stmt); static void dump_commit(PLpgSQL_stmt_commit *stmt); static void dump_rollback(PLpgSQL_stmt_rollback *stmt); +static void dump_let(PLpgSQL_stmt_let *stmt); static void dump_expr(PLpgSQL_expr *expr); @@ -914,6 +927,9 @@ dump_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: dump_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + dump_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -1590,6 +1606,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt) printf("\n"); } +static void +dump_let(PLpgSQL_stmt_let *stmt) +{ + dump_ind(); + dump_expr(stmt->expr); + printf("\n"); +} + static void dump_expr(PLpgSQL_expr *expr) { diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 8182ce28aa1..3f155c4f8a4 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -200,7 +200,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %type <stmt> stmt_return stmt_raise stmt_assert stmt_execsql %type <stmt> stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag %type <stmt> stmt_open stmt_fetch stmt_move stmt_close stmt_null -%type <stmt> stmt_commit stmt_rollback +%type <stmt> stmt_commit stmt_rollback stmt_let %type <stmt> stmt_case stmt_foreach_a %type <list> proc_exceptions @@ -307,6 +307,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %token <keyword> K_INTO %token <keyword> K_IS %token <keyword> K_LAST +%token <keyword> K_LET %token <keyword> K_LOG %token <keyword> K_LOOP %token <keyword> K_MERGE @@ -876,6 +877,8 @@ proc_stmt : pl_block ';' { $$ = $1; } | stmt_rollback { $$ = $1; } + | stmt_let + { $$ = $1; } ; stmt_perform : K_PERFORM @@ -992,6 +995,29 @@ stmt_assign : T_DATUM } ; +stmt_let : K_LET + { + PLpgSQL_stmt_let *new; + RawParseMode pmode; + + pmode = RAW_PARSE_PLPGSQL_LET; + + new = palloc0(sizeof(PLpgSQL_stmt_let)); + new->cmd_type = PLPGSQL_STMT_LET; + new->lineno = plpgsql_location_to_lineno(@1); + new->stmtid = ++plpgsql_curr_compile->nstatements; + + /* push back the head name to include it in the stmt */ + plpgsql_push_back_token(K_LET); + new->expr = read_sql_construct(';', 0, 0, ";", + pmode, + false, true, + NULL, NULL); + + $$ = (PLpgSQL_stmt *)new; + } + ; + stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' { PLpgSQL_stmt_getdiag *new; diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h index d338e9f6374..be94aa751a4 100644 --- a/src/pl/plpgsql/src/pl_reserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h @@ -40,6 +40,7 @@ PG_KEYWORD("from", K_FROM) PG_KEYWORD("if", K_IF) PG_KEYWORD("in", K_IN) PG_KEYWORD("into", K_INTO) +PG_KEYWORD("let", K_LET) PG_KEYWORD("loop", K_LOOP) PG_KEYWORD("not", K_NOT) PG_KEYWORD("null", K_NULL) diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h index 670b4cf0c1e..602628d7d2a 100644 --- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h @@ -69,6 +69,7 @@ PG_KEYWORD("info", K_INFO) PG_KEYWORD("insert", K_INSERT) PG_KEYWORD("is", K_IS) PG_KEYWORD("last", K_LAST) +PG_KEYWORD("let", K_LET) PG_KEYWORD("log", K_LOG) PG_KEYWORD("merge", K_MERGE) PG_KEYWORD("message", K_MESSAGE) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 50c3b28472b..bb400629adb 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -128,6 +128,7 @@ typedef enum PLpgSQL_stmt_type PLPGSQL_STMT_CALL, PLPGSQL_STMT_COMMIT, PLPGSQL_STMT_ROLLBACK, + PLPGSQL_STMT_LET, } PLpgSQL_stmt_type; /* @@ -520,6 +521,17 @@ typedef struct PLpgSQL_stmt_assign PLpgSQL_expr *expr; } PLpgSQL_stmt_assign; +/* + * Let statement + */ +typedef struct PLpgSQL_stmt_let +{ + PLpgSQL_stmt_type cmd_type; + int lineno; + unsigned int stmtid; + PLpgSQL_expr *expr; +} PLpgSQL_stmt_let; + /* * PERFORM statement */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 64ddf589b3c..fe60f2916f4 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1890,6 +1890,7 @@ PLpgSQL_stmt_forq PLpgSQL_stmt_fors PLpgSQL_stmt_getdiag PLpgSQL_stmt_if +PLpgSQL_stmt_let PLpgSQL_stmt_loop PLpgSQL_stmt_open PLpgSQL_stmt_perform -- 2.47.0 [text/x-patch] v20241119-2-0015-allow-parallel-execution-queries-with-session-variab.patch (11.9K, 10-v20241119-2-0015-allow-parallel-execution-queries-with-session-variab.patch) download | inline diff: From 644d333fca884762d32b182b8fd8883fddab7de9 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:08:17 +0100 Subject: [PATCH 15/22] allow parallel execution queries with session variables --- doc/src/sgml/parallel.sgml | 6 - src/backend/executor/execMain.c | 14 +- src/backend/executor/execParallel.c | 147 +++++++++++++++++- src/backend/optimizer/util/clauses.c | 18 +-- .../regress/expected/session_variables.out | 12 +- 5 files changed, 171 insertions(+), 26 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 683dede6adc..1ce9abf86f5 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,12 +515,6 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> - - <listitem> - <para> - Plan nodes that use a session variable. - </para> - </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 21e938a6227..783716c4e57 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -212,7 +212,19 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) * be changed inside query execution time, and then a reference to * previously returned value can be corrupted). */ - if (queryDesc->plannedstmt->sessionVariables) + if (queryDesc->num_session_variables > 0) + { + /* + * When a parallel query needs to access query parameters (including + * related session variables), then related session variables are + * restored (deserialized) in queryDesc already. So just push pointer + * of this array to executor's estate. + */ + Assert(IsParallelWorker()); + estate->es_session_variables = queryDesc->session_variables; + estate->es_num_session_variables = queryDesc->num_session_variables; + } + else if (queryDesc->plannedstmt->sessionVariables) { ListCell *lc; int nSessionVariables; diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c index bfb3419efb7..cb434e2768e 100644 --- a/src/backend/executor/execParallel.c +++ b/src/backend/executor/execParallel.c @@ -12,8 +12,9 @@ * workers and ensuring that their state generally matches that of the * leader; see src/backend/access/transam/README.parallel for details. * However, we must save and restore relevant executor state, such as - * any ParamListInfo associated with the query, buffer/WAL usage info, and - * the actual plan to be passed down to the worker. + * any ParamListInfo associated with the query, buffer/WAL usage info, + * session variables buffer, and the actual plan to be passed down to + * the worker. * * IDENTIFICATION * src/backend/executor/execParallel.c @@ -64,6 +65,7 @@ #define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xE000000000000008) #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009) #define PARALLEL_KEY_WAL_USAGE UINT64CONST(0xE00000000000000A) +#define PARALLEL_KEY_SESSION_VARIABLES UINT64CONST(0xE00000000000000B) #define PARALLEL_TUPLE_QUEUE_SIZE 65536 @@ -138,6 +140,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate, /* Helper function that runs in the parallel worker. */ static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc); +/* Helper functions that can pass values of session variables */ +static Size EstimateSessionVariables(EState *estate); +static void SerializeSessionVariables(EState *estate, char **start_address); +static SessionVariableValue *RestoreSessionVariables(char **start_address, + int *num_session_variables); + /* * Create a serialized representation of the plan to be sent to each worker. */ @@ -596,6 +604,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, char *pstmt_data; char *pstmt_space; char *paramlistinfo_space; + char *session_variables_space; BufferUsage *bufusage_space; WalUsage *walusage_space; SharedExecutorInstrumentation *instrumentation = NULL; @@ -605,6 +614,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, int instrumentation_len = 0; int jit_instrumentation_len = 0; int instrument_offset = 0; + int session_variables_len = 0; Size dsa_minsize = dsa_minimum_size(); char *query_string; int query_len; @@ -660,6 +670,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len); shm_toc_estimate_keys(&pcxt->estimator, 1); + /* Estimate space for serialized session variables. */ + session_variables_len = EstimateSessionVariables(estate); + shm_toc_estimate_chunk(&pcxt->estimator, session_variables_len); + shm_toc_estimate_keys(&pcxt->estimator, 1); + /* * Estimate space for BufferUsage. * @@ -761,6 +776,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space); SerializeParamList(estate->es_param_list_info, ¶mlistinfo_space); + /* Store serialized session variables. */ + session_variables_space = shm_toc_allocate(pcxt->toc, session_variables_len); + shm_toc_insert(pcxt->toc, PARALLEL_KEY_SESSION_VARIABLES, session_variables_space); + SerializeSessionVariables(estate, &session_variables_space); + /* Allocate space for each worker's BufferUsage; no need to initialize. */ bufusage_space = shm_toc_allocate(pcxt->toc, mul_size(sizeof(BufferUsage), pcxt->nworkers)); @@ -1411,6 +1431,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) SharedJitInstrumentation *jit_instrumentation; int instrument_options = 0; void *area_space; + char *sessionvariable_space; dsa_area *area; ParallelWorkerContext pwcxt; @@ -1436,6 +1457,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false); area = dsa_attach_in_place(area_space, seg); + /* Reconstruct session variables. */ + sessionvariable_space = shm_toc_lookup(toc, + PARALLEL_KEY_SESSION_VARIABLES, + false); + queryDesc->session_variables = + RestoreSessionVariables(&sessionvariable_space, + &queryDesc->num_session_variables); + /* Start up the executor */ queryDesc->plannedstmt->jitFlags = fpes->jit_flags; ExecutorStart(queryDesc, fpes->eflags); @@ -1504,3 +1533,117 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) FreeQueryDesc(queryDesc); receiver->rDestroy(receiver); } + +/* + * Estimate the amount of space required to serialize a session variable. + */ +static Size +EstimateSessionVariables(EState *estate) +{ + int i; + Size sz = sizeof(int); + + if (estate->es_session_variables == NULL) + return sz; + + for (i = 0; i < estate->es_num_session_variables; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + + typeOid = svarval->typid; + + sz = add_size(sz, sizeof(Oid)); /* space for type OID */ + + /* space for datum/isnull */ + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + sz = add_size(sz, + datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen)); + } + + return sz; +} + +/* + * Serialize a session variables buffer into caller-provided storage. + * + * We write the number of parameters first, as a 4-byte integer, and then + * write details for each parameter in turn. The details for each parameter + * consist of a 4-byte type OID, and then the datum as serialized by + * datumSerialize(). The caller is responsible for ensuring that there is + * enough storage to store the number of bytes that will be written; use + * EstimateSessionVariables to find out how many will be needed. + * *start_address is updated to point to the byte immediately following those + * written. + * + * RestoreSessionVariables can be used to recreate a session variable buffer + * based on the serialized representation; + */ +static void +SerializeSessionVariables(EState *estate, char **start_address) +{ + int nparams; + int i; + + /* Write number of parameters. */ + nparams = estate->es_num_session_variables; + memcpy(*start_address, &nparams, sizeof(int)); + *start_address += sizeof(int); + + /* Write each parameter in turn. */ + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + typeOid = svarval->typid; + + /* Write type OID. */ + memcpy(*start_address, &typeOid, sizeof(Oid)); + *start_address += sizeof(Oid); + + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + datumSerialize(svarval->value, svarval->isnull, typByVal, typLen, + start_address); + } +} + +static SessionVariableValue * +RestoreSessionVariables(char **start_address, int *num_session_variables) +{ + SessionVariableValue *session_variables; + int i; + int nparams; + + memcpy(&nparams, *start_address, sizeof(int)); + *start_address += sizeof(int); + + *num_session_variables = nparams; + session_variables = (SessionVariableValue *) + palloc(nparams * sizeof(SessionVariableValue)); + + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval = &session_variables[i]; + + /* Read type OID. */ + memcpy(&svarval->typid, *start_address, sizeof(Oid)); + *start_address += sizeof(Oid); + + /* Read datum/isnull. */ + svarval->value = datumRestore(start_address, &svarval->isnull); + } + + return session_variables; +} diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index a5452c0c9e4..8cee732561d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -924,25 +924,19 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) /* * We can't pass Params to workers at the moment either, so they are also - * parallel-restricted, unless they are PARAM_EXTERN Params or are - * PARAM_EXEC Params listed in safe_param_ids, meaning they could be - * either generated within workers or can be computed by the leader and - * then their value can be passed to workers. + * parallel-restricted, unless they are PARAM_EXTERN or PARAM_VARIABLE + * Params or are PARAM_EXEC Params listed in safe_param_ids, meaning they + * could be either generated within workers or can be computed by the + * leader and then their value can be passed to workers. */ else if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind == PARAM_EXTERN) + if (param->paramkind == PARAM_EXTERN || + param->paramkind == PARAM_VARIABLE) return false; - /* we don't support passing session variables to workers */ - if (param->paramkind == PARAM_VARIABLE) - { - if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) - return true; - } - if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2f1708f7c84..8df3a91c05e 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -644,12 +644,14 @@ SELECT count(*) FROM svar_test WHERE a%10 = zero; -- parallel execution is not supported yet EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; - QUERY PLAN ------------------------------------ + QUERY PLAN +-------------------------------------------- Aggregate - -> Seq Scan on svar_test - Filter: ((a % 10) = zero) -(3 rows) + -> Gather + Workers Planned: 2 + -> Parallel Seq Scan on svar_test + Filter: ((a % 10) = zero) +(5 rows) LET zero = (SELECT count(*) FROM svar_test); -- result should be 1000 -- 2.47.0 [text/x-patch] v20241119-2-0014-allow-read-an-value-of-session-variable-directly-fro.patch (13.3K, 11-v20241119-2-0014-allow-read-an-value-of-session-variable-directly-fro.patch) download | inline diff: From 3456ed8963f41c59ff32426c01f2506913c0fc36 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:07:03 +0100 Subject: [PATCH 14/22] allow read an value of session variable directly from expression executor The expression executor can be called directly (not inside query executor) when expression is evaluated as simple expression in plpgsql or when expression is an argument of CALL or EXECUTE statements. For these cases we need to allow direct access to content of session variables from executor. We can do it, because we know so the expression is not evaluated under query and cannot be evaluated inside parallel worker. This patch enables session variables for simple expression evaluation. This patch enables usage session variables in arguments of CALL stmt. --- src/backend/commands/prepare.c | 8 -- src/backend/executor/execExpr.c | 82 ++++++++++++++----- src/backend/executor/execExprInterp.c | 16 ++++ src/backend/jit/llvm/llvmjit_expr.c | 6 ++ src/backend/parser/analyze.c | 8 -- src/include/executor/execExpr.h | 12 ++- .../src/expected/plpgsql_session_variable.out | 3 +- src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 26 +++--- src/test/regress/sql/session_variables.sql | 12 +-- 10 files changed, 117 insertions(+), 59 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 3735b5554e0..e8c375df198 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,14 +338,6 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } - /* - * The arguments of EXECUTE are evaluated by a direct expression - * executor call. This mode doesn't support session variables yet. - * It will be enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6dc8c562bec..845bdd1ae85 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1005,25 +1005,69 @@ ExecInitExprRec(Expr *node, ExprState *state, es_num_session_variables = state->parent->state->es_num_session_variables; } - Assert(es_session_variables); - - /* parameter sanity checks */ - if (param->paramid >= es_num_session_variables) - elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); - - var = &es_session_variables[param->paramid]; - - if (var->typid != param->paramtype) - elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); - - /* - * In this case, pass the value like a - * constant. - */ - scratch.opcode = EEOP_CONST; - scratch.d.constval.value = var->value; - scratch.d.constval.isnull = var->isnull; - ExprEvalPushStep(state, &scratch); + if (es_session_variables) + { + /* + * This path is used when expression is + * evaluated inside query evaluation. For + * ensuring of immutability of session variable + * inside query we use special buffer with copy + * of used session variables. This method helps + * with parallel execution too. + */ + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + else + { + AclResult aclresult; + Oid varid = param->paramvarid; + Oid vartype = param->paramtype; + + Assert(!IsParallelWorker()); + + /* + * In some cases (plpgsql's simple expression + * evaluation or evaluation of CALL arguments), + * the expression executor is called directly. + * We can allow direct access to session + * variables (copy only), because we know, so + * outside is not any query (and expression + * cannot be executed parallel). + * + * In this case we should to do aclcheck, + * because usual aclcheck from + * standard_ExecutorStart is not executed in + * this case. Fortunately it is just once per + * transaction. + */ + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + scratch.opcode = EEOP_PARAM_VARIABLE; + scratch.d.vparam.varid = varid; + scratch.d.vparam.vartype = vartype; + ExprEvalPushStep(state, &scratch); + } } break; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 30c5a19aad6..0a154b97d6e 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -59,6 +59,7 @@ #include "access/heaptoast.h" #include "catalog/pg_type.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -450,6 +451,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_PARAM_EXEC, &&CASE_EEOP_PARAM_EXTERN, &&CASE_EEOP_PARAM_CALLBACK, + &&CASE_EEOP_PARAM_VARIABLE, &&CASE_EEOP_PARAM_SET, &&CASE_EEOP_CASE_TESTVAL, &&CASE_EEOP_MAKE_READONLY, @@ -1099,6 +1101,20 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_PARAM_VARIABLE) + { + /* + * Direct access to session variable (without buffering). Because + * returned value can be used (without an assignement) after the + * referenced session variables is updated, we have to use an copy + * of stored value every time. + */ + *op->resvalue = GetSessionVariableWithTypeCheck(op->d.vparam.varid, + op->resnull, + op->d.vparam.vartype); + EEO_NEXT(); + } + EEO_CASE(EEOP_PARAM_SET) { /* out of line, unlikely to matter performance-wise */ diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 9cec17544b8..e8cc54351ed 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1136,6 +1136,12 @@ llvm_compile_expr(ExprState *state) break; } + case EEOP_PARAM_VARIABLE: + build_EvalXFunc(b, mod, "ExecEvalParamVariable", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_PARAM_SET: build_EvalXFunc(b, mod, "ExecEvalParamSet", v_state, op, v_econtext); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 9dfd435128a..496d75a494a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3470,14 +3470,6 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); - /* - * The arguments of CALL statement are evaluated by a direct expression - * executor call. This path is unsupported yet, so block it. It will be - * enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index cd97dfa0624..72b54b3a33f 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -156,10 +156,11 @@ typedef enum ExprEvalOp EEOP_BOOLTEST_IS_FALSE, EEOP_BOOLTEST_IS_NOT_FALSE, - /* evaluate PARAM_EXEC/EXTERN parameters */ + /* evaluate PARAM_EXEC/EXTERN/VARIABLE parameters */ EEOP_PARAM_EXEC, EEOP_PARAM_EXTERN, EEOP_PARAM_CALLBACK, + EEOP_PARAM_VARIABLE, /* set PARAM_EXEC value */ EEOP_PARAM_SET, @@ -409,6 +410,13 @@ typedef struct ExprEvalStep Oid paramtype; /* OID of parameter's datatype */ } cparam; + /* for EEOP_PARAM_VARIABLE */ + struct + { + Oid varid; /* OID of assigned variable */ + Oid vartype; /* OID of parameter's datatype */ + } vparam; + /* for EEOP_CASE_TESTVAL/DOMAIN_TESTVAL */ struct { @@ -831,6 +839,8 @@ extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op); extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op); extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index a6523f7afe2..ecac64fcfb4 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -211,8 +211,7 @@ SET ROLE TO regress_var_exec_role; -- should fail SELECT var_read_func(); ERROR: permission denied for session variable plpgsql_sv_var1 -CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" -PL/pgSQL function var_read_func() line 3 at RAISE +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE SET ROLE TO DEFAULT; SET ROLE TO regress_var_owner_role; GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 1361a8a8480..86c5bd324a9 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,8 +8099,7 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations || - query->hasSessionVariables) + query->setOperations) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a8520e5579d..2f1708f7c84 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -108,8 +108,7 @@ ERROR: permission denied for session variable var1 CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: PL/pgSQL expression "$1 + var1" -PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN -- should be ok SELECT sqlfx_sd(20); sqlfx_sd @@ -279,27 +278,28 @@ $$; NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); -ERROR: session variable cannot be used as an argument +NOTICE: 1 DO $$ BEGIN CALL p(v); END $$; -ERROR: session variable cannot be used as an argument -CONTEXT: SQL statement "CALL p(v)" -PL/pgSQL function inline_code_block line 1 at CALL +NOTICE: 1 DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); -ERROR: session variable cannot be used as an argument + ?column? +---------- + 20 +(1 row) + DEALLOCATE ptest; DROP VARIABLE v; -- test search path diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index ac8e2cb0943..fcd783e966c 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -207,13 +207,14 @@ $$; DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; + CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); DO $$ BEGIN CALL p(v); END $$; @@ -221,13 +222,12 @@ DO $$ BEGIN CALL p(v); END $$; DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); DEALLOCATE ptest; -- 2.47.0 [text/x-patch] v20241119-2-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch (35.6K, 12-v20241119-2-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch) download | inline diff: From 95d56145c19788d5054a456a9ea4978b49741d32 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:05:06 +0100 Subject: [PATCH 13/22] Implementation of NOT NULL and IMMUTABLE clauses Almost trivial patch - psql, pg_dump support --- doc/src/sgml/catalogs.sgml | 20 +++ doc/src/sgml/plpgsql.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 32 +++- src/backend/catalog/pg_variable.c | 8 + src/backend/commands/session_variable.c | 69 +++++++- src/backend/parser/gram.y | 41 +++-- src/bin/pg_dump/pg_dump.c | 19 ++- src/bin/pg_dump/pg_dump.h | 2 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++ src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/pg_variable.h | 6 + src/include/nodes/parsenodes.h | 2 + src/test/regress/expected/psql.out | 36 ++-- .../regress/expected/session_variables.out | 155 +++++++++++++++++- src/test/regress/sql/session_variables.sql | 122 ++++++++++++++ 16 files changed, 523 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 124902c4e44..9a8886fc69a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9842,6 +9842,26 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <row> <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnotnull</structfield> <type>boolean</type> + </para> + <para> + True if the session variable doesn't allow null values. The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varisimmutable</structfield> <type>boolean</type> + </para> + <para> + True if the variable is <link linkend="sql-createvariable-immutable">immutable</link> (cannot be modified). + The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vareoxaction</structfield> <type>char</type> <structfield>varxactendaction</structfield> <type>char</type> </para> <para> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index bfbff9ab74e..e704e05a991 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6044,7 +6044,8 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; You can consider translating an Oracle package into a schema in <productname>PostgreSQL</productname>. Package functions and procedures would then become functions and procedures in that schema, and package - variables could be translated to session variables in that schema. + variables could be translated to session variables or immutable session + variables in that schema. (see <xref linkend="ddl-session-variables"/>). </para> </sect3> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 8187c18e28b..00938743c0d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,8 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] +CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -65,6 +65,22 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <variablelist> + <varlistentry id="sql-createvariable-immutable"> + <term><literal>IMMUTABLE</literal></term> + <listitem> + <para> + The assigned value of the session variable can not be changed. + Only if the session variable doesn't have a default value, a single + initialization is allowed using the <command>LET</command> command. Once + done, no further change is allowed until end of transaction + if the session variable was created with clause <literal>ON TRANSACTION + END RESET</literal>, or until reset of all session variables by + <command>DISCARD VARIABLES</command>, or until reset of all session + objects by command <command>DISCARD ALL</command>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-if-not-exists"> <term><literal>IF NOT EXISTS</literal></term> <listitem> @@ -105,6 +121,18 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-not-null"> + <term><literal>NOT NULL</literal></term> + <listitem> + <para> + The <literal>NOT NULL</literal> clause forbids setting the session + variable to a null value. A session variable created as NOT NULL + should not to have a declared default value, and if the variable + has not assigned value, then the reading of this variable fails. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-default"> <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> <listitem> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 36de12587c0..f261e3fd770 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -40,6 +40,8 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -55,6 +57,8 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -117,6 +121,8 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); + values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -259,6 +265,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + stmt->not_null, + stmt->is_immutable, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0a1c326e72e..7f34f93b542 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -84,6 +84,9 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool not_null; + bool is_immutable; + bool reset_at_eox; /* @@ -102,6 +105,9 @@ typedef struct SVariableData bool is_valid; uint32 hashvalue; /* used for pairing sinval message */ + + /* true, when the value is already set, and cannot be changed more */ + bool protect_value; } SVariableData; typedef SVariableData *SVariable; @@ -563,6 +569,23 @@ eval_assign_defexpr(SVariable svar, HeapTuple tup) MemoryContextSwitchTo(oldcxt); } + else + { + /* + * Raise an error if this is a NOT NULL variable but the result of + * DEFAULT expression is NULL. + */ + if (svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)), + errdetail("The result of DEFAULT expression is NULL."))); + } + + if (svar->is_immutable) + svar->protect_value = true; FreeExecutorState(estate); } @@ -593,6 +616,9 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + svar->not_null = varform->varnotnull; + svar->is_immutable = varform->varisimmutable; + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; @@ -622,9 +648,31 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); - if (!is_write) + svar->protect_value = false; + + /* + * When the variable is marked as IMMUTABLE, we prefer to evaluate + * possible DEFAULT before write op. In this case we want to protect + * default value against any overwrite. + */ + if (!is_write || + svar->is_immutable) + { eval_assign_defexpr(svar, tup); + /* + * Raise an error if this is a NOT NULL variable without default + * expression. + */ + if (svar->isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("The session variable was not initialized yet."))); + } + ReleaseSysCache(tup); } @@ -644,6 +692,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Assert(svar); Assert(!isnull || value == (Datum) 0); + if (svar->protect_value) + ereport(ERROR, + (errcode(ERRCODE_ERROR_IN_ASSIGNMENT), + errmsg("session variable \"%s.%s\" is declared IMMUTABLE", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + + /* don't allow assignment of null to NOT NULL variable */ + if (isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + /* * Use typbyval, typbylen from session variable only when they are * trustworthy (the invalidation message was not accepted for this @@ -684,6 +747,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + + /* don't allow more changes of value when variable is IMMUTABLE */ + if (svar->is_immutable) + svar->protect_value = true; } /* diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b8e33ed19db..6aea3bd35b2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,6 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <boolean> OptNotNull OptImmutable /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -5231,27 +5232,31 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $4->relpersistence = $2; - n->variable = $4; - n->typeName = $6; - n->collClause = (CollateClause *) $7; - n->defexpr = $8; - n->XactEndAction = $9; + $5->relpersistence = $2; + n->is_immutable = $3; + n->variable = $5; + n->typeName = $7; + n->collClause = (CollateClause *) $8; + n->not_null = $9; + n->defexpr = $10; + n->XactEndAction = $11; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $7->relpersistence = $2; - n->variable = $7; - n->typeName = $9; - n->collClause = (CollateClause *) $10; - n->defexpr = $11; - n->XactEndAction = $12; + $8->relpersistence = $2; + n->is_immutable = $3; + n->variable = $8; + n->typeName = $10; + n->collClause = (CollateClause *) $11; + n->not_null = $12; + n->defexpr = $13; + n->XactEndAction = $14; n->if_not_exists = true; $$ = (Node *) n; } @@ -5271,6 +5276,14 @@ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } ; +OptNotNull: NOT NULL_P { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + +OptImmutable: IMMUTABLE { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c138994304a..59f4103ca4f 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5437,6 +5437,8 @@ getVariables(Archive *fout) int i_varxactendaction; int i_varowner; int i_varcollation; + int i_varnotnull; + int i_varisimmutable; int i_varacl; int i_acldefault; int i, @@ -5459,6 +5461,8 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " v.varnotnull,\n" + " v.varisimmutable,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5479,6 +5483,8 @@ getVariables(Archive *fout) i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); + i_varnotnull = PQfnumber(res, "varnotnull"); + i_varisimmutable = PQfnumber(res, "varisimmutable"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5505,6 +5511,8 @@ getVariables(Archive *fout) pg_strdup(PQgetvalue(res, i, i_varxactendaction)); varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; + varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5551,7 +5559,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vartypname; const char *vardefexpr; const char *varxactendaction; + const char *varisimmutable; Oid varcollation; + bool varnotnull; /* skip if not to be dumped */ if (!varinfo->dobj.dump || dopt->dataOnly) @@ -5565,12 +5575,14 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; + varnotnull = varinfo->varnotnull; + varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", - qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", + varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { @@ -5582,6 +5594,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (varnotnull) + appendPQExpBuffer(query, " NOT NULL"); + if (vardefexpr) appendPQExpBuffer(query, " DEFAULT %s", vardefexpr); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 8ed83979ec9..f7921f942d0 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -717,6 +717,8 @@ typedef struct _VariableInfo char *initrvaracl; Oid varcollation; const char *rolname; /* name of owner, or empty string */ + bool varnotnull; + bool varisimmutable; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index f58ede5334a..47cb59356e0 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4041,6 +4041,42 @@ my %tests = ( }, }, + 'CREATE IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE IMMUTABLE VARIABLE test_variable NOT NULL DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dc18da350eb..cc69f07f1f9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,8 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " NOT v.varnotnull as \"%s\",\n" + " NOT v.varisimmutable as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5237,6 +5239,8 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Nullable"), + gettext_noop("Mutable"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 3fb2c4e9248..b653dda1fe8 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1273,6 +1273,8 @@ static const pgsql_thing_t words_after_create[] = { {"FOREIGN TABLE", NULL, NULL, NULL}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, + {"IMMUTABLE VARIABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE IMMUTABLE + * VARIABLE ... */ {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP}, @@ -3593,7 +3595,8 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); + COMPLETE_WITH("IMMUTABLE VARIABLE", "SEQUENCE", "TABLE", "VARIABLE", + "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3945,7 +3948,8 @@ match_previous_words(int pattern_id, /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ else if (TailMatches("CREATE", "VARIABLE", MatchAny) || - TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ @@ -4589,6 +4593,10 @@ match_previous_words(int pattern_id, else if (TailMatches("FROM", "SERVER", MatchAny, "INTO", MatchAny)) COMPLETE_WITH("OPTIONS ("); +/* IMMUTABLE -- can be expend to IMMUTABLE VARIABLE */ + else if (TailMatches("CREATE", "IMMUTABLE")) + COMPLETE_WITH("VARIABLE"); + /* INSERT --- can be inside EXPLAIN, RULE, etc */ /* Complete NOT MATCHED THEN INSERT */ else if (TailMatches("NOT", "MATCHED", "THEN", "INSERT")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index e8bfeebed11..68bc49a0e27 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,12 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* don't allow NULL */ + bool varnotnull BKI_DEFAULT(f); + + /* don't allow changes */ + bool varisimmutable BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 60404413c49..7308c323574 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,8 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + bool not_null; /* disallow nulls */ + bool is_immutable; /* don't allow changes */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index bc7b60a97ea..77a7bf45c8a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index ee153f8fb82..a8520e5579d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1617,3 +1617,148 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The session variable was not initialized yet. +--should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1; +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; +--should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DISCARD VARIABLES; +-- should to fail +LET var1 = NULL; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DROP VARIABLE var1; +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The result of DEFAULT expression is NULL. +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; +-- should to fail +LET var1 = 10; +ERROR: session variable "public.var1" is declared IMMUTABLE +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should to fail +LET var1 = 30; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 9d9d04ec76b..ac8e2cb0943 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1086,3 +1086,125 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); + +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; + +--should be ok +LET var1 = 10; +SELECT var1; + +DROP VARIABLE var1; + +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; + +--should be ok +SELECT var1; + +-- should be ok +LET var1 = 10; +SELECT var1; + +DISCARD VARIABLES; + +-- should to fail +LET var1 = NULL; + +DROP VARIABLE var1; + +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); + +-- should to fail +SELECT var1; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +SELECT var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); + +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; + +-- should be ok +SELECT var1; +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; + +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; + +-- should to fail +LET var1 = 10; +LET var1 = 20; + +-- should be ok +SELECT var1; + +-- should to fail +LET var1 = 30; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] v20241119-2-0012-Implementation-of-DEFAULT-clause-default-expressions.patch (33.6K, 13-v20241119-2-0012-Implementation-of-DEFAULT-clause-default-expressions.patch) download | inline diff: From f62ba800864cd94bde0249b637b9e5485c26a16e Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:03:53 +0100 Subject: [PATCH 12/22] Implementation of DEFAULT clause - default expressions for session variables When the evaluation of default expression fails, we remove related entry from sessionvars hash table. Then sessionvars can contain only sucessfully initialized values. Then we don't need special flag for badly initialized session variables. --- doc/src/sgml/catalogs.sgml | 8 ++ doc/src/sgml/ddl.sgml | 16 ++-- doc/src/sgml/ref/create_variable.sgml | 21 +++-- doc/src/sgml/ref/discard.sgml | 3 +- src/backend/catalog/pg_variable.c | 27 ++++++ src/backend/commands/session_variable.c | 87 ++++++++++++++++++- src/backend/parser/gram.y | 15 +++- src/backend/parser/parse_agg.c | 2 + src/backend/parser/parse_expr.c | 4 + src/backend/parser/parse_func.c | 1 + src/bin/pg_dump/pg_dump.c | 14 +++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++++++ src/bin/psql/describe.c | 4 +- src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/parse_node.h | 1 + src/test/regress/expected/psql.out | 36 ++++---- .../regress/expected/session_variables.out | 73 ++++++++++++++-- src/test/regress/sql/session_variables.sql | 48 ++++++++++ 20 files changed, 355 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 02b6b27c5cb..124902c4e44 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9874,6 +9874,14 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vardefexpr</structfield> <type>pg_node_tree</type> + </para> + <para> + The internal representation of the variable default value + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 049a31a6da2..600c153ee29 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5339,14 +5339,14 @@ SELECT current_user_id; <para> The value of a session variable is local to the current session. Retrieving - a variable's value returns a <literal>NULL</literal>, unless its value has - been set to something else in the current session using the - <command>LET</command> command. Session variables are not transactional: - any changes made to the value of a session variable in a transaction won't - be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves can be persistent - or temporary, but their values are neither persistent nor shared (like the - content of temporary tables). + a variable's value returns a <literal>NULL</literal> or a default value, + unless its value has been set to something else in the current session + using the <command>LET</command> command. Session variables are not + transactional: any changes made to the value of a session variable in + a transaction won't be undone if the transaction is rolled back (just like + variables in procedural languages). Session variables themselves can be + persistent or temporary, but their values are neither persistent nor shared + (like the content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index e1c2940d4ca..8187c18e28b 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] + [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -41,10 +41,10 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <para> The value of a session variable is local to the current session. Retrieving - a session variable's value returns NULL, unless its value is set to - something else in the current session with a <command>LET</command> command. - The content of a session variable is not transactional. This is the same as - regular variables in procedural languages. + a session variable's value returns NULL or a default value, unless its value + is set to something else in the current session with a <command>LET</command> + command. The content of a session variable is not transactional. This is the + same as regular variables in procedural languages. </para> <para> @@ -105,6 +105,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-default"> + <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> + <listitem> + <para> + The <literal>DEFAULT</literal> clause can be used to assign a default + value to a session variable. This expression is evaluated when the session + variable is first accessed for reading and had not yet been assigned a value. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> <term><literal>ON COMMIT DROP</literal></term> <listitem> diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index 61b967f9c9b..6b0cb95034f 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -71,7 +71,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } <listitem> <para> Resets the value of all session variables. If a variable - is later reused, it is re-initialized to <literal>NULL</literal>. + is later reused, it is re-initialized to either + <literal>NULL</literal> or its default value. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 4520eed6da8..36de12587c0 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -24,6 +24,9 @@ #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "miscadmin.h" +#include "parser/parse_coerce.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -37,6 +40,7 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -51,6 +55,7 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction) { Acl *varacl; @@ -114,6 +119,11 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); + if (varDefexpr) + values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr)); + else + nulls[Anum_pg_variable_vardefexpr - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); if (varacl != NULL) @@ -150,6 +160,11 @@ create_variable(const char *varName, record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); free_object_addresses(addrs); + /* dependency on default expr */ + if (varDefexpr) + recordDependencyOnExpr(&myself, (Node *) varDefexpr, + NIL, DEPENDENCY_NORMAL); + /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); @@ -185,6 +200,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid collation; Oid typcollation; ObjectAddress variable; + Node *cooked_default = NULL; /* check consistency of arguments */ if (stmt->XactEndAction == VARIABLE_XACTEND_DROP @@ -226,6 +242,16 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) format_type_be(typid)), parser_errposition(pstate, stmt->collClause->location))); + if (stmt->defexpr) + { + cooked_default = transformExpr(pstate, stmt->defexpr, + EXPR_KIND_VARIABLE_DEFAULT); + + cooked_default = coerce_to_specific_type(pstate, + cooked_default, typid, "DEFAULT"); + assign_expr_collations(pstate, cooked_default); + } + variable = create_variable(stmt->variable->relname, namespaceid, typid, @@ -233,6 +259,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + cooked_default, stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0b512815d4a..0a1c326e72e 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/svariableReceiver.h" #include "funcapi.h" #include "miscadmin.h" +#include "optimizer/optimizer.h" #include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" @@ -510,11 +511,68 @@ AtEOSubXact_SessionVariables(bool isCommit, } } +/* + * evaluate an expression + */ +static void +eval_assign_defexpr(SVariable svar, HeapTuple tup) +{ + Datum defexpr_value; + bool isnull; + + Assert(svar); + Assert(svar->is_valid); + Assert(HeapTupleIsValid(tup)); + + defexpr_value = SysCacheGetAttr(VARIABLEOID, + tup, + Anum_pg_variable_vardefexpr, + &isnull); + + if (!isnull) + { + EState *estate; + ExprState *defexprs; + Expr *defexpr; + char *defexpr_str; + Datum value; + MemoryContext oldcxt; + + estate = CreateExecutorState(); + + defexpr_str = TextDatumGetCString(defexpr_value); + defexpr = (Expr *) stringToNode(defexpr_str); + + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + + defexpr = expression_planner((Expr *) defexpr); + defexprs = ExecInitExpr(defexpr, NULL); + + value = ExecEvalExprSwitchContext(defexprs, + GetPerTupleExprContext(estate), + &isnull); + + MemoryContextSwitchTo(oldcxt); + + if (!isnull) + { + oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + svar->value = datumCopy(value, svar->typbyval, svar->typlen); + svar->isnull = false; + + MemoryContextSwitchTo(oldcxt); + } + + FreeExecutorState(estate); + } +} + /* * Initialize attributes cached in "svar" */ static void -setup_session_variable(SVariable svar, Oid varid) +setup_session_variable(SVariable svar, Oid varid, bool is_write) { HeapTuple tup; Form_pg_variable varform; @@ -564,6 +622,9 @@ setup_session_variable(SVariable svar, Oid varid) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!is_write) + eval_assign_defexpr(svar, tup); + ReleaseSysCache(tup); } @@ -593,7 +654,7 @@ set_session_variable(SVariable svar, Datum value, bool isnull) */ if (!svar->is_valid) { - setup_session_variable(&locsvar, svar->varid); + setup_session_variable(&locsvar, svar->varid, false); _svar = &locsvar; } else @@ -726,7 +787,25 @@ get_session_variable(Oid varid) */ if (!svar->is_valid) { - setup_session_variable(svar, varid); + /* in this case we want to use defexp if it is defined */ + PG_TRY(); + { + /* + * In this case, the setup can execute default expression. When + * the execution of default expression fails, then we need to + * remove entry from session vars. + */ + setup_session_variable(svar, varid, false); + } + PG_CATCH(); + { + /* this entry cannot be valid, remove from sessionvars */ + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + + /* propagate the error */ + PG_RE_THROW(); + } + PG_END_TRY(); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", get_namespace_name(get_session_variable_namespace(varid)), @@ -790,7 +869,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!found) { - setup_session_variable(svar, varid); + setup_session_variable(svar, varid, true); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", get_namespace_name(get_session_variable_namespace(svar->varid)), diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d8de8acf944..b8e33ed19db 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -638,6 +638,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <partboundspec> PartitionBoundSpec %type <list> hash_partbound %type <defelt> hash_partbound_elem +%type <node> OptSessionVarDefExpr %type <node> json_format_clause json_format_clause_opt @@ -5230,30 +5231,36 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $4->relpersistence = $2; n->variable = $4; n->typeName = $6; n->collClause = (CollateClause *) $7; - n->XactEndAction = $8; + n->defexpr = $8; + n->XactEndAction = $9; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $7->relpersistence = $2; n->variable = $7; n->typeName = $9; n->collClause = (CollateClause *) $10; - n->XactEndAction = $11; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = true; $$ = (Node *) n; } ; +OptSessionVarDefExpr: DEFAULT b_expr { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + /* * Temporary session variables can be dropped on successful * transaction end like tables. diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index a7343001f21..ddfbcf42b74 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -488,6 +488,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: if (isAgg) err = _("aggregate functions are not allowed in DEFAULT expressions"); @@ -937,6 +938,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("window functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 3602c3572d7..bcbb1f564af 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -582,6 +582,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind) case EXPR_KIND_JOIN_USING: case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: result = false; break; } @@ -667,6 +668,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: case EXPR_KIND_LET_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: /* okay */ break; @@ -2075,6 +2077,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("cannot use subquery in DEFAULT expression"); break; case EXPR_KIND_INDEX_EXPRESSION: @@ -3427,6 +3430,7 @@ ParseExprKindName(ParseExprKind exprKind) return "CHECK"; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: return "DEFAULT"; case EXPR_KIND_INDEX_EXPRESSION: return "index expression"; diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9aa4f60768b..fcac694455d 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2620,6 +2620,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("set-returning functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 2ec6a22a266..c138994304a 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_vardefexpr; int i_varxactendaction; int i_varowner; int i_varcollation; @@ -5458,6 +5459,7 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" "FROM pg_catalog.pg_variable v\n" @@ -5474,6 +5476,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); @@ -5509,6 +5512,11 @@ getVariables(Archive *fout) varinfo[i].dacl.initprivs = NULL; varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + if (PQgetisnull(res, i, i_vardefexpr)) + varinfo[i].vardefexpr = NULL; + else + varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr)); + /* do not try to dump ACL if no ACL exists */ if (!PQgetisnull(res, i, i_varacl)) varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; @@ -5541,6 +5549,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *vardefexpr; const char *varxactendaction; Oid varcollation; @@ -5553,6 +5562,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; @@ -5572,6 +5582,10 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (vardefexpr) + appendPQExpBuffer(query, " DEFAULT %s", + vardefexpr); + if (strcmp(varxactendaction, "r") == 0) appendPQExpBuffer(query, " ON TRANSACTION END RESET"); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7c0c03749e8..8ed83979ec9 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *vardefexpr; char *varxactendaction; char *varacl; char *rvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 419ea39727b..f58ede5334a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4005,6 +4005,42 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE VARIABLE test_variable DEFAULT ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d0f69f8c8e9..dc18da350eb 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,7 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" @@ -5236,6 +5237,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Default"), gettext_noop("Transactional end action")); if (verbose) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 60291ef7ac2..e8bfeebed11 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -67,6 +67,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* access permissions */ aclitem varacl[1] BKI_DEFAULT(_null_); + /* list of expression trees for variable default (NULL if none) */ + pg_node_tree vardefexpr BKI_DEFAULT(_null_); + #endif } FormData_pg_variable; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7a519e08429..60404413c49 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index c11fdb3d970..fa566b194d1 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -84,6 +84,7 @@ typedef enum ParseExprKind EXPR_KIND_CYCLE_MARK, /* cycle mark value */ EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ EXPR_KIND_LET_TARGET, /* LET target */ + EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */ } ParseExprKind; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 61077a52b16..bc7b60a97ea 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 99e4ac72be1..ee153f8fb82 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1554,3 +1554,66 @@ SELECT var1 IS NULL; (1 row) DROP VARIABLE var1; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); +ERROR: cannot drop function vartest_fx() because other objects depend on it +DETAIL: session variable var1 depends on function vartest_fx() +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +SELECT var1; +NOTICE: vartest_fx executed + var1 +------ + 0 +(1 row) + +-- the defexpr should be evaluated only once +SELECT var1; + var1 +------ + 0 +(1 row) + +DISCARD VARIABLES; +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DISCARD VARIABLES; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +-- should to fail, but not to crash +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- again +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- but we can write +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 7853261080b..9d9d04ec76b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1038,3 +1038,51 @@ ROLLBACK; SELECT var1 IS NULL; DROP VARIABLE var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); + +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); + +-- should be ok +SELECT var1; + +-- the defexpr should be evaluated only once +SELECT var1; + +DISCARD VARIABLES; + +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + +DISCARD VARIABLES; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +-- should to fail, but not to crash +SELECT var1; + +-- again +SELECT var1; + +-- but we can write +LET var1 = 100; +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); -- 2.47.0 [text/x-patch] v20241119-2-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch (14.6K, 14-v20241119-2-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch) download | inline diff: From 1428307b22c5d9c379a84dfa5f17cd3bffdaf0f2 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:02:13 +0100 Subject: [PATCH 11/22] Implementation ON TRANSACTION END RESET clause This is simple patch - just add special flag to session variable memory entry. The entries with active this flag are removed from sessionvars hash table at transaction end. The "TRANSACTION END" is synonyms for "COMMIT ROLLBACK" but the "TRANSACTION END" is more illustrative and less confusing then "COMMIT ROLLBACK". --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 13 ++++- src/backend/commands/session_variable.c | 51 +++++++++++++++++++ src/backend/parser/gram.y | 1 + src/bin/pg_dump/pg_dump.c | 11 ++++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 18 +++++++ src/bin/psql/describe.c | 1 + src/include/catalog/pg_variable.h | 1 + .../isolation/expected/session-variable.out | 4 +- .../isolation/specs/session-variable.spec | 4 +- .../regress/expected/session_variables.out | 34 +++++++++++++ src/test/regress/sql/session_variables.sql | 20 ++++++++ 13 files changed, 158 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 94db502b72e..02b6b27c5cb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9846,7 +9846,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para> <para> Action performed at end of transaction: - <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + <literal>n</literal> = no action, <literal>d</literal> = drop the variable, + <literal>r</literal> = reset the variable to its default value. </para></entry> </row> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 642346186f0..e1c2940d4ca 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ ON COMMIT DROP ] + [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -117,6 +117,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-transaction-end-reset"> + <term><literal>ON TRANSACTION END RESET</literal></term> + <listitem> + <para> + The <literal>ON TRANSACTION END RESET</literal> clause causes the session + variable to be reset to its default value when the transaction is committed + or rolled back. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index b23ae21ba55..0b512815d4a 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -83,6 +83,8 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool reset_at_eox; + /* * Top level local transaction id of the last transaction that dropped the * variable, if any. We need this information to avoid freeing memory for @@ -110,6 +112,12 @@ static MemoryContext SVariableMemoryContext = NULL; /* becomes true when we receive a sinval message */ static bool needs_validation = false; +/* + * true, when some used session variable has ON COMMIT DROP + * or ON TRANSACTION END RESET clauses + */ +static bool has_session_variables_with_reset_at_eox = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -385,6 +393,32 @@ remove_invalid_session_variables(bool atEOX) } } +/* + * remove entries marked as "reset_at_eox" + */ +static void +remove_session_variables_with_reset_at_eox(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_session_variables_with_reset_at_eox) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->reset_at_eox) + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + } + + has_session_variables_with_reset_at_eox = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -392,6 +426,8 @@ remove_invalid_session_variables(bool atEOX) void AtPreEOXact_SessionVariables(bool isCommit) { + remove_session_variables_with_reset_at_eox(); + if (isCommit) { if (xact_drop_items) @@ -503,6 +539,21 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + /* + * We don't need to explicitly reset variables marked ON COMMIT DROP. It + * can be done by sinval message processing. But this processing can be + * postponed due aborted transaction. On second hand there is not a + * reason, why don't do it at transaction end immediately. + */ + if (varform->varxactendaction == VARIABLE_XACTEND_RESET || + varform->varxactendaction == VARIABLE_XACTEND_DROP) + { + svar->reset_at_eox = true; + has_session_variables_with_reset_at_eox = true; + } + else + svar->reset_at_eox = false; + svar->drop_lxid = InvalidTransactionId; svar->isnull = true; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 76655aa8aeb..d8de8acf944 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5259,6 +5259,7 @@ CreateSessionVarStmt: * transaction end like tables. */ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | ON TRANSACTION END_P RESET { $$ = VARIABLE_XACTEND_RESET; } | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } ; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b8d839ece41..2ec6a22a266 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_varxactendaction; int i_varowner; int i_varcollation; int i_varacl; @@ -5450,6 +5451,7 @@ getVariables(Archive *fout) /* get the variables in current database */ appendPQExpBuffer(query, "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varxactendaction,\n" " v.varnamespace, v.vartype,\n" " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" " CASE WHEN v.varcollation <> t.typcollation " @@ -5472,6 +5474,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); i_varowner = PQfnumber(res, "varowner"); @@ -5495,6 +5498,9 @@ getVariables(Archive *fout) varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varxactendaction = + pg_strdup(PQgetvalue(res, i, i_varxactendaction)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); @@ -5535,6 +5541,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *varxactendaction; Oid varcollation; /* skip if not to be dumped */ @@ -5546,6 +5553,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", @@ -5564,6 +5572,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (strcmp(varxactendaction, "r") == 0) + appendPQExpBuffer(query, " ON TRANSACTION END RESET"); + appendPQExpBuffer(query, ";\n"); if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f2f558b2961..7c0c03749e8 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *varxactendaction; char *varacl; char *rvaracl; char *initvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index dd8c054a6a6..419ea39727b 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3987,6 +3987,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d02f6416f0e..d0f69f8c8e9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 1e6dd98d415..60291ef7ac2 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -74,6 +74,7 @@ typedef enum VariableXactEndAction { VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ + VARIABLE_XACTEND_RESET = 'r', /* ON TRANSACTION END RESET */ } VariableXactEndAction; /* ---------------- diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index a609797dc5d..e9a254bcd12 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,11 +86,12 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; +step o_eox_r: CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; @@ -103,6 +104,7 @@ t step discard: DISCARD VARIABLES; step sc3: COMMIT; +step clean: DROP VARIABLE myvar_o_eox_r; step state: SELECT varname FROM pg_variable; varname ------- diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index 45e65d4085d..5d089c8a4ed 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -25,12 +25,14 @@ session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } +step o_eox_r { CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } step discard { DISCARD VARIABLES; } step sc3 { COMMIT; } +step clean { DROP VARIABLE myvar_o_eox_r; } step state { SELECT varname FROM pg_variable; } session s4 @@ -48,4 +50,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 6dadb9074da..99e4ac72be1 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1520,3 +1520,37 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be NULL; +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be NULL +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 770ce650685..7853261080b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1018,3 +1018,23 @@ COMMIT; SELECT count(*) FROM pg_variable WHERE varname = 'var1'; -- should be zero SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; + +BEGIN; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be NULL; +SELECT var1 IS NULL; + +BEGIN; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be NULL +SELECT var1 IS NULL; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] v20241119-2-0009-PREPARE-LET-support.patch (7.4K, 15-v20241119-2-0009-PREPARE-LET-support.patch) download | inline diff: From 55b3107be6fef19f9387599ef9ec589bdb74a44e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 09:44:58 +0100 Subject: [PATCH 09/22] 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.in.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 <replaceable class="parameter">name</replaceable> [ ( <replaceable class <listitem> <para> Any <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, - <command>DELETE</command>, <command>MERGE</command>, or <command>VALUES</command> - statement. + <command>DELETE</command>, <command>MERGE</command>, <command>VALUES</command>, + or <command>LET</command> statement. </para> </listitem> </varlistentry> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index def7dde2330..af511d12aa1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12161,7 +12161,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.in.c b/src/bin/psql/tab-complete.in.c index 3ef4776d98a..df3e2539270 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4629,9 +4629,9 @@ match_previous_words(int pattern_id, 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 <variable> 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 a867fdd3e75..cc4b5964799 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -439,9 +439,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 @@ -1321,5 +1321,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 8a6dbffd54e..65c2a79425d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -893,5 +893,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.47.0 [text/x-patch] v20241119-2-0010-implementation-of-temporary-session-variables.patch (42.2K, 16-v20241119-2-0010-implementation-of-temporary-session-variables.patch) download | inline diff: From 005996b9e9a828709ea525ef4a199a3905296e2f Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:00:28 +0100 Subject: [PATCH 10/22] implementation of temporary session variables The temporary variables are created inside temp schema and they are dropped by dropping this schema. For consistency with temp tables, the CREATE TEMP VARIABLE command supports ON COMMIT DROP clause. The temporary variables with this clause are collected in xact_drop_items list. This list should be carefully maintained and can contain only valid entries (not yet dropped). From this reasons now the subcommit and subaborting have to be handled. --- doc/src/sgml/catalogs.sgml | 10 + doc/src/sgml/ddl.sgml | 6 +- doc/src/sgml/ref/create_variable.sgml | 15 +- src/backend/access/transam/xact.c | 9 + src/backend/catalog/pg_variable.c | 24 +- src/backend/commands/session_variable.c | 218 +++++++++++++++++- src/backend/commands/view.c | 2 +- src/backend/parser/analyze.c | 4 +- src/backend/parser/gram.y | 30 ++- src/backend/parser/parse_relation.c | 22 +- src/bin/psql/describe.c | 10 +- src/bin/psql/tab-complete.in.c | 5 +- src/include/catalog/pg_variable.h | 9 + src/include/commands/session_variable.h | 6 +- src/include/nodes/parsenodes.h | 1 + src/include/nodes/primnodes.h | 4 +- src/include/parser/parse_relation.h | 2 +- .../isolation/expected/session-variable.out | 3 +- .../isolation/specs/session-variable.spec | 3 +- src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 130 ++++++++++- src/test/regress/sql/session_variables.sql | 66 ++++++ src/tools/pgindent/typedefs.list | 1 + 23 files changed, 546 insertions(+), 70 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 0c15d56dd42..94db502b72e 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9840,6 +9840,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varxactendaction</structfield> <type>char</type> + </para> + <para> + Action performed at end of transaction: + <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varcollation</structfield> <type>oid</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index ce1b1e1f599..049a31a6da2 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5344,9 +5344,9 @@ SELECT current_user_id; <command>LET</command> command. Session variables are not transactional: any changes made to the value of a session variable in a transaction won't be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves are persistent, but - their values are neither persistent nor shared (like the content of - temporary tables). + procedural languages). Session variables themselves can be persistent + or temporary, but their values are neither persistent nor shared (like the + content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 05bf67e3411..642346186f0 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ ON COMMIT DROP ] </synopsis> </refsynopsisdiv> <refsect1> @@ -104,6 +105,18 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> + <term><literal>ON COMMIT DROP</literal></term> + <listitem> + <para> + The <literal>ON COMMIT DROP</literal> clause specifies the behaviour of a + temporary session variable at transaction commit. With this clause, the + session variable is dropped at commit time. The clause is only allowed + for temporary variables. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b7ebcc2a557..01e3d469306 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -36,6 +36,7 @@ #include "catalog/pg_enum.h" #include "catalog/storage.h" #include "commands/async.h" +#include "commands/session_variable.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "common/pg_prng.h" @@ -2316,6 +2317,9 @@ CommitTransaction(void) */ smgrDoPendingSyncs(true, is_parallel_worker); + /* remove values of dropped session variables from memory */ + AtPreEOXact_SessionVariables(true); + /* close large objects before lower-level cleanup */ AtEOXact_LargeObject(true); @@ -2912,6 +2916,7 @@ AbortTransaction(void) AtAbort_Portals(); smgrDoPendingSyncs(false, is_parallel_worker); AtEOXact_LargeObject(false); + AtPreEOXact_SessionVariables(false); AtAbort_Notify(); AtEOXact_RelationMap(false, is_parallel_worker); AtAbort_Twophase(); @@ -5190,6 +5195,8 @@ CommitSubTransaction(void) AtEOSubXact_SPI(true, s->subTransactionId); AtEOSubXact_on_commit_actions(true, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(true, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(true, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(true, s->subTransactionId, @@ -5355,6 +5362,8 @@ AbortSubTransaction(void) AtEOSubXact_SPI(false, s->subTransactionId); AtEOSubXact_on_commit_actions(false, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(false, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(false, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(false, s->subTransactionId, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index c7369e2b2ac..4520eed6da8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -36,7 +36,8 @@ static ObjectAddress create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists); + bool if_not_exists, + VariableXactEndAction varXactEndAction); /* @@ -49,7 +50,8 @@ create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists) + bool if_not_exists, + VariableXactEndAction varXactEndAction) { Acl *varacl; NameData varname; @@ -110,6 +112,7 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); @@ -183,6 +186,13 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid typcollation; ObjectAddress variable; + /* check consistency of arguments */ + if (stmt->XactEndAction == VARIABLE_XACTEND_DROP + && stmt->variable->relpersistence != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("ON COMMIT DROP can only be used on temporary variables"))); + namespaceid = RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); @@ -222,7 +232,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) typmod, varowner, collation, - stmt->if_not_exists); + stmt->if_not_exists, + stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", stmt->variable->relname, variable.objectId); @@ -230,6 +241,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) /* we want SessionVariableCreatePostprocess to see the catalog changes. */ CommandCounterIncrement(); + SessionVariableCreatePostprocess(variable.objectId, stmt->XactEndAction); + return variable; } @@ -242,6 +255,7 @@ DropVariableById(Oid varid) { Relation rel; HeapTuple tup; + char XactEndAction; rel = table_open(VariableRelationId, RowExclusiveLock); @@ -250,6 +264,8 @@ DropVariableById(Oid varid) if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for variable %u", varid); + XactEndAction = ((Form_pg_variable) GETSTRUCT(tup))->varxactendaction; + CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); @@ -257,5 +273,5 @@ DropVariableById(Oid varid) table_close(rel, RowExclusiveLock); /* do the necessary cleanup in local memory, if needed */ - SessionVariableDropPostprocess(varid); + SessionVariableDropPostprocess(varid, XactEndAction); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 21c7a9517b5..b23ae21ba55 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -16,6 +16,8 @@ #include "access/xact.h" #include "catalog/pg_variable.h" +#include "catalog/dependency.h" +#include "catalog/namespace.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" #include "funcapi.h" @@ -31,6 +33,19 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" +typedef struct SVariableXActDropItem +{ + Oid varid; /* varid of session variable */ + + /* + * creating_subid is the ID of the creating subxact. If the action was + * unregistered during the current transaction, deleting_subid is the ID + * of the deleting subxact, otherwise InvalidSubTransactionId. + */ + SubTransactionId creating_subid; + SubTransactionId deleting_subid; +} SVariableXActDropItem; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -96,13 +111,21 @@ static MemoryContext SVariableMemoryContext = NULL; static bool needs_validation = false; /* - * The content of dropped session variables is not removed immediately. We do - * that in the next transaction that reads or writes a session variable. - * "validated_lxid" stores the transaction that performed said validation, so - * that we can avoid repeating the effort. + * The content of dropped session variables is not removed immediately. If + * possible, we do that at the end of the transaction. But we cannot do that + * if the transaction aborted, because we lost access to the system catalog. + * In that case, we clean up in the next transaction that reads or writes a + * session variable. "validated_lxid" stores the transaction that performed + * said validation, so that we can avoid repeating the effort. */ static LocalTransactionId validated_lxid = InvalidLocalTransactionId; +/* list holds fields of SVariableXActDropItem type */ +static List *xact_drop_items = NIL; + +static void register_session_variable_xact_drop(Oid varid); +static void unregister_session_variable_xact_drop(Oid varid); + /* * Callback function for session variable invalidation. */ @@ -139,16 +162,45 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) } } +/* + * Do the necessary work to setup local memory management of a new + * variable. + * + * Caller should already have created the necessary entry in catalog + * and made them visible. + */ +void +SessionVariableCreatePostprocess(Oid varid, char XactEndAction) +{ + /* + * For temporary variables, we need to create a new end of xact action to + * ensure deletion from catalog. + */ + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + register_session_variable_xact_drop(varid); + } +} + /* * Handle the local memory cleanup for a DROP VARIABLE command. * * Caller should take care of removing the pg_variable entry first. */ void -SessionVariableDropPostprocess(Oid varid) +SessionVariableDropPostprocess(Oid varid, char XactEndAction) { Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + unregister_session_variable_xact_drop(varid); + } + if (sessionvars) { bool found; @@ -170,6 +222,57 @@ SessionVariableDropPostprocess(Oid varid) } } +/* + * Registration of actions to be executed on session variables at transaction + * end time. We want to drop temporary session variables with clause ON COMMIT + * DROP. + */ + +/* + * Register a session variable xact action. + */ +static void +register_session_variable_xact_drop(Oid varid) +{ + SVariableXActDropItem *xact_ai; + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + + xact_ai = (SVariableXActDropItem *) + palloc(sizeof(SVariableXActDropItem)); + + xact_ai->varid = varid; + + xact_ai->creating_subid = GetCurrentSubTransactionId(); + xact_ai->deleting_subid = InvalidSubTransactionId; + + xact_drop_items = lcons(xact_ai, xact_drop_items); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Unregister an id of a given session variable from drop list. In this + * moment, the action is just marked as deleted by setting deleting_subid. The + * calling even might be rollbacked, in which case we should not lose this + * action. + */ +static void +unregister_session_variable_xact_drop(Oid varid) +{ + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + if (xact_ai->varid == varid) + xact_ai->deleting_subid = GetCurrentSubTransactionId(); + } +} + /* * Release stored value, free memory */ @@ -222,9 +325,11 @@ is_session_variable_valid(SVariable svar) /* * Check all potentially invalid session variable data in local memory and free * the memory for all invalid ones. This function is called before any read or - * write of a session variable. Freeing of a variable's memory is postponed if - * the variable has been dropped by the current transaction, since that - * operation could still be rolled back. + * write of a session variable or when the transaction ends. At the end of a + * transaction (atEOX is true) we can discard all invalid variables. Inside a + * transaction (atEOX is false) we postpone freeing a variable's memory if the + * variable has been dropped by the current transaction, since that operation + * could still be rolled back. * * It is possible that we receive a cache invalidation message while * remove_invalid_session_variables() is executing, so we cannot guarantee that @@ -232,7 +337,7 @@ is_session_variable_valid(SVariable svar) * done. However, we can guarantee that all entries get checked once. */ static void -remove_invalid_session_variables(void) +remove_invalid_session_variables(bool atEOX) { HASH_SEQ_STATUS status; SVariable svar; @@ -259,7 +364,7 @@ remove_invalid_session_variables(void) { if (!svar->is_valid) { - if (svar->drop_lxid == MyProc->vxid.lxid) + if (!atEOX && svar->drop_lxid == MyProc->vxid.lxid) { /* try again in the next transaction */ needs_validation = true; @@ -280,6 +385,95 @@ remove_invalid_session_variables(void) } } +/* + * Perform ON COMMIT DROP for temporary session variables, + * and remove all dropped variables from memory. + */ +void +AtPreEOXact_SessionVariables(bool isCommit) +{ + if (isCommit) + { + if (xact_drop_items) + { + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + /* iterate only over entries that are still pending */ + if (xact_ai->deleting_subid == InvalidSubTransactionId) + { + ObjectAddress object; + + object.classId = VariableRelationId; + object.objectId = xact_ai->varid; + object.objectSubId = 0; + + /* + * Since this is an automatic drop, rather than one + * directly initiated by the user, we pass the + * PERFORM_DELETION_INTERNAL flag. + */ + elog(DEBUG1, "session variable (oid:%u) will be deleted (forced by ON COMMIT DROP clause)", + xact_ai->varid); + + performDeletion(&object, DROP_CASCADE, + PERFORM_DELETION_INTERNAL | + PERFORM_DELETION_QUIETLY); + } + } + } + + remove_invalid_session_variables(true); + } + + /* + * We have to clean xact_drop_items. All related variables are dropped + * now, or lost inside aborted transaction. + */ + list_free_deep(xact_drop_items); + xact_drop_items = NULL; +} + +/* + * Post-subcommit or post-subabort cleanup of xact drop list. + * + * During subabort, we can immediately remove entries created during this + * subtransaction. During subcommit, just transfer entries marked during + * this subtransaction as being the parent's responsibility. + */ +void +AtEOSubXact_SessionVariables(bool isCommit, + SubTransactionId mySubid, + SubTransactionId parentSubid) +{ + ListCell *cur_item; + + foreach(cur_item, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(cur_item); + + if (!isCommit && xact_ai->creating_subid == mySubid) + { + /* cur_item must be removed */ + xact_drop_items = foreach_delete_current(xact_drop_items, cur_item); + pfree(xact_ai); + } + else + { + /* cur_item must be preserved */ + if (xact_ai->creating_subid == mySubid) + xact_ai->creating_subid = parentSubid; + if (xact_ai->deleting_subid == mySubid) + xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -438,7 +632,7 @@ get_session_variable(Oid varid) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; @@ -534,7 +728,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 2bd49eb55e6..c736c5c46f3 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -484,7 +484,7 @@ DefineView(ViewStmt *stmt, const char *queryString, */ view = copyObject(stmt->view); /* don't corrupt original command */ if (view->relpersistence == RELPERSISTENCE_PERMANENT - && isQueryUsingTempRelation(viewParse)) + && isQueryUsingTempObject(viewParse)) { view->relpersistence = RELPERSISTENCE_TEMP; ereport(NOTICE, diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 81b8b0a633d..9dfd435128a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3390,10 +3390,10 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) * creation query. It would be hard to refresh data or incrementally * maintain it if a source disappeared. */ - if (isQueryUsingTempRelation(query)) + if (isQueryUsingTempObject(query)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("materialized views must not use temporary tables or views"))); + errmsg("materialized views must not use temporary tables, views or session variables"))); /* * A materialized view would either need to save parameters for use in diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af511d12aa1..76655aa8aeb 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -466,6 +466,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <ival> OptTemp %type <ival> OptNoLog %type <oncommit> OnCommitOption +%type <ival> XactEndActionOption %type <ival> for_locking_strength %type <node> for_locking_item @@ -5229,26 +5230,39 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $3; - n->typeName = $5; - n->collClause = (CollateClause *) $6; + $4->relpersistence = $2; + n->variable = $4; + n->typeName = $6; + n->collClause = (CollateClause *) $7; + n->XactEndAction = $8; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $6; - n->typeName = $8; - n->collClause = (CollateClause *) $9; + $7->relpersistence = $2; + n->variable = $7; + n->typeName = $9; + n->collClause = (CollateClause *) $10; + n->XactEndAction = $11; n->if_not_exists = true; $$ = (Node *) n; } ; +/* + * Temporary session variables can be dropped on successful + * transaction end like tables. + */ +XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } + ; + + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 8075b1b8a1b..95ae2108044 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -101,7 +101,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); -static bool isQueryUsingTempRelation_walker(Node *node, void *context); +static bool isQueryUsingTempObject_walker(Node *node, void *context); /* @@ -3896,13 +3896,13 @@ rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte) * the query is a temporary relation (table, view, or materialized view). */ bool -isQueryUsingTempRelation(Query *query) +isQueryUsingTempObject(Query *query) { - return isQueryUsingTempRelation_walker((Node *) query, NULL); + return isQueryUsingTempObject_walker((Node *) query, NULL); } static bool -isQueryUsingTempRelation_walker(Node *node, void *context) +isQueryUsingTempObject_walker(Node *node, void *context) { if (node == NULL) return false; @@ -3928,13 +3928,23 @@ isQueryUsingTempRelation_walker(Node *node, void *context) } return query_tree_walker(query, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context, QTW_IGNORE_JOINALIASES); } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (isAnyTempNamespace(get_session_variable_namespace(p->paramvarid))) + return true; + } + } return expression_tree_walker(node, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context); } diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index c52c8fd147f..d02f6416f0e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5226,12 +5226,16 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" - " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " CASE v.varxactendaction\n" + " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), gettext_noop("Type"), gettext_noop("Collation"), - gettext_noop("Owner")); + gettext_noop("Owner"), + gettext_noop("Transactional end action")); if (verbose) { diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index df3e2539270..3fb2c4e9248 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3593,7 +3593,7 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW"); + COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3944,7 +3944,8 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ - else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + else if (TailMatches("CREATE", "VARIABLE", MatchAny) || + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index db593cd5bed..1e6dd98d415 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* action on transaction end */ + char varxactendaction BKI_DEFAULT(n); + /* variable collation */ Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); @@ -67,6 +70,12 @@ CATALOG(pg_variable,9222,VariableRelationId) #endif } FormData_pg_variable; +typedef enum VariableXactEndAction +{ + VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ + VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ +} VariableXactEndAction; + /* ---------------- * Form_pg_variable corresponds to a pointer to a tuple with * the format of the pg_variable relation. diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 3dab8ae2a48..dc83296b1ba 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,7 +21,11 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" -extern void SessionVariableDropPostprocess(Oid varid); +extern void SessionVariableCreatePostprocess(Oid varid, char XactEndAction); +extern void SessionVariableDropPostprocess(Oid varid, char XactEndAction); +extern void AtPreEOXact_SessionVariables(bool isCommit); +extern void AtEOSubXact_SessionVariables(bool isCommit, SubTransactionId mySubid, + SubTransactionId parentSubid); extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 04ab22bd492..7a519e08429 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 4b2424d6d34..6174a5562c5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -51,7 +51,9 @@ typedef struct Alias List *colnames; /* optional list of column aliases */ } Alias; -/* What to do at commit time for temporary relations */ +/* + * What to do at commit time for temporary relations or session variables. + */ typedef enum OnCommitAction { ONCOMMIT_NOOP, /* No ON COMMIT clause (do nothing) */ diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index 91fd8e243b5..a8117bcae3f 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -126,6 +126,6 @@ extern int attnameAttNum(Relation rd, const char *attname, bool sysColOK); extern const NameData *attnumAttName(Relation rd, int attid); extern Oid attnumTypeId(Relation rd, int attid); extern Oid attnumCollationId(Relation rd, int attid); -extern bool isQueryUsingTempRelation(Query *query); +extern bool isQueryUsingTempObject(Query *query); #endif /* PARSE_RELATION_H */ diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index 0a5579dc7ce..a609797dc5d 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,10 +86,11 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; +step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index c864fee4006..45e65d4085d 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -24,6 +24,7 @@ step create { CREATE VARIABLE myvar AS text; } session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } +step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } @@ -47,4 +48,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 88e21194711..61077a52b16 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index cc4b5964799..6dadb9074da 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| - | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1400,3 +1400,123 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; +NOTICE: view "var_test_view" will be a temporary view +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view var_test_view +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 65c2a79425d..770ce650685 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -826,6 +826,7 @@ BEGIN; DROP VARIABLE var1; SELECT var2; ROLLBACK; + -- should be ok SELECT var1; @@ -952,3 +953,68 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; + +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; + +DROP VARIABLE var1 CASCADE; + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 68e3c3c7b63..64ddf589b3c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2798,6 +2798,7 @@ SupportRequestWFuncMonotonic SVariable SVariableData SVariableState +SVariableXActDropItem Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] v20241119-2-0008-EXPLAIN-LET-support.patch (8.2K, 17-v20241119-2-0008-EXPLAIN-LET-support.patch) download | inline diff: From c5f9c60c3b5f4d39290226f478ca15aad747e20e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 18:28:07 +0200 Subject: [PATCH 08/22] EXPLAIN LET support Enhancing ExplainOnePlan is necessary to be EXPLAIN ANALYZE LET fully workable. In this case we want to be result of query or expression written to target variable. --- doc/src/sgml/ref/explain.sgml | 3 +- src/backend/commands/explain.c | 31 ++++++++++++-- src/backend/commands/prepare.c | 5 ++- src/backend/parser/gram.y | 3 +- src/include/commands/explain.h | 3 +- .../regress/expected/session_variables.out | 40 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 21 ++++++++++ 7 files changed, 97 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml index db9d3a8549a..caccc70658c 100644 --- a/doc/src/sgml/ref/explain.sgml +++ b/doc/src/sgml/ref/explain.sgml @@ -98,7 +98,8 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>, <command>MERGE</command>, <command>CREATE TABLE AS</command>, - or <command>EXECUTE</command> statement + <command>EXECUTE</command>, + or <command>LET</command> statement without letting the command affect your data, use this approach: <programlisting> BEGIN; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 7c0fd63b2f0..d41516454f5 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -18,6 +18,7 @@ #include "commands/createas.h" #include "commands/defrem.h" #include "commands/prepare.h" +#include "executor/svariableReceiver.h" #include "foreign/fdwapi.h" #include "jit/jit.h" #include "libpq/pqformat.h" @@ -512,8 +513,9 @@ standard_ExplainOneQuery(Query *query, int cursorOptions, } /* run it (if needed) and produce output */ - ExplainOnePlan(plan, into, es, queryString, params, queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(plan, into, query->resultVariable, es, queryString, + params, queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); } @@ -611,6 +613,25 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, else ExplainDummyGroup("Notify", NULL, es); } + else if (IsA(utilityStmt, LetStmt)) + { + LetStmt *letstmt = (LetStmt *) utilityStmt; + List *rewritten; + Query *query; + + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "SET SESSION VARIABLE\n"); + else + ExplainDummyGroup("Set Session Variable", NULL, es); + + rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query))); + + Assert(list_length(rewritten) == 1); + query = linitial_node(Query, rewritten); + ExplainOneQuery(query, + CURSOR_OPT_PARALLEL_OK, NULL, es, + pstate, params); + } else { if (es->format == EXPLAIN_FORMAT_TEXT) @@ -634,8 +655,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, * to call it. */ void -ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params, +ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, Oid targetvar, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, const BufferUsage *bufusage, const MemoryContextCounters *mem_counters) @@ -684,6 +705,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, */ if (into) dest = CreateIntoRelDestReceiver(into); + else if (OidIsValid(targetvar)) + dest = CreateVariableDestReceiver(targetvar); else if (es->serialize != EXPLAIN_SERIALIZE_NONE) dest = CreateExplainSerializeDestReceiver(es); else diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 455a03cce63..3735b5554e0 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -662,8 +662,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es, PlannedStmt *pstmt = lfirst_node(PlannedStmt, p); if (pstmt->commandType != CMD_UTILITY) - ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(pstmt, into, InvalidOid, es, query_string, paramLI, + pstate->p_queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); else ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index aad47305f20..def7dde2330 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12130,7 +12130,8 @@ ExplainableStmt: | CreateAsStmt | CreateMatViewStmt | RefreshMatViewStmt - | ExecuteStmt /* by default all are $$=$1 */ + | ExecuteStmt + | LetStmt /* by default all are $$=$1 */ ; /***************************************************************************** diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index aa5872bc154..f5b08857ae8 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -103,7 +103,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, ParseState *pstate, ParamListInfo params); -extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, +extern void ExplainOnePlan(PlannedStmt *plannedstmt, + IntoClause *into, Oid targetvar, ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 50fb478b5e9..a867fdd3e75 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1283,3 +1283,43 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; NOTICE: drop cascades to session variable xxtab.avar +CREATE VARIABLE var1 bigint; +CREATE TABLE var_tab_test_table(a int); +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); +VACUUM ANALYZE var_tab_test_table; +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index e7a4fdedae4..8a6dbffd54e 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -874,3 +874,24 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; + +CREATE VARIABLE var1 bigint; + +CREATE TABLE var_tab_test_table(a int); + +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); + +VACUUM ANALYZE var_tab_test_table; + +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be NULL +SELECT var1; + +EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be 10 +SELECT var1; + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; -- 2.47.0 [text/x-patch] v20241119-2-0006-plpgsql-tests.patch (16.9K, 18-v20241119-2-0006-plpgsql-tests.patch) download | inline diff: From c47353ac91f7e0849c61c3907e80e0dade9422cf Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 06/22] plpgsql tests --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 382 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 289 +++++++++++++ 4 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ab779900596 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,382 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; +LET plpgsql_sv_var = pi(); +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; +NOTICE: value of session variable is 3.14159265358979 +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; +SELECT plpgsql_sv_var AS "pi_multiply_2"; + pi_multiply_2 +------------------ + 6.28318530717959 +(1 row) + +DROP VARIABLE plpgsql_sv_var; +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; +-- execute in a transaction +BEGIN; +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +END; +-- execute outside of a transaction +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP FUNCTION test_func(); +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +SET ROLE TO regress_var_owner_role; +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_reader_role; +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail +SELECT var_read_func(); +ERROR: permission denied for session variable plpgsql_sv_var1 +CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" +PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should be ok +SELECT var_read_func(); +NOTICE: 10 + var_read_func +--------------- + +(1 row) + +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +DROP VARIABLE plpgsql_sv_var1; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail, but not crash +SELECT var_read_func(); +ERROR: column "plpgsql_sv_var1" does not exist +LINE 1: plpgsql_sv_var1 + ^ +QUERY: plpgsql_sv_var1 +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +DROP FUNCTION var_read_func; +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_int(1); + inc_var_int +------------- + 1 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 2 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 3 +(1 row) + +SELECT inc_var_int(1) FROM generate_series(1,10); + inc_var_int +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +LET plpgsql_sv_var2 = 0.0; +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_num(1.0); + inc_var_num +------------- + 1 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 2 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 3 +(1 row) + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + inc_var_num +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +NOTICE: variable is 1000 +DROP VARIABLE plpgsql_sv_var1; +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +select ffunc(); + ffunc +------- + abc +(1 row) + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); +DROP VARIABLE plpgsql_sv_v; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 3dd734b776b..3905c0b6d45 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..a3cc264cf38 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,289 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; + +LET plpgsql_sv_var = pi(); + +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; + +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; + +SELECT plpgsql_sv_var AS "pi_multiply_2"; + +DROP VARIABLE plpgsql_sv_var; + +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; + +-- execute in a transaction +BEGIN; +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); +END; + +-- execute outside of a transaction +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; + +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +SELECT test_func(); + +DROP FUNCTION test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; + +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +SET ROLE TO regress_var_owner_role; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_reader_role; + +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should be ok +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; + +DROP VARIABLE plpgsql_sv_var1; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail, but not crash +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION var_read_func; + +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; + +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; + +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_int(1); +SELECT inc_var_int(1); +SELECT inc_var_int(1); + +SELECT inc_var_int(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; + +LET plpgsql_sv_var2 = 0.0; + +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; + +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +DROP VARIABLE plpgsql_sv_var1; + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; + +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +select ffunc(); + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); + +DROP VARIABLE plpgsql_sv_v; -- 2.47.0 [text/x-patch] v20241119-2-0007-GUC-session_variables_ambiguity_warning.patch (13.9K, 19-v20241119-2-0007-GUC-session_variables_ambiguity_warning.patch) download | inline diff: From f7732df8ddd50c62f52c9c170a884f5e888239d6 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 17:55:24 +0200 Subject: [PATCH 07/22] GUC session_variables_ambiguity_warning Inside an query the session variables can be shadowed. This behaviour is by design, because this ensuring so new variables doesn't break existing queries. But this behaviour can be confusing, because shadowing is quiet. When new GUC session_variables_ambiguity_warning is on, then the warning is displayed, when some session variable can be used, but it is not used due shadowing. This feature should to help with investigation of unexpected behaviour. Note: PLpgSQL has an option that controls priority of identifier's spaces (SQL or PLpgSQL). Default behaviour doesn't allows collisions. I believe this default is the best. I cannot to implement similar very strict behaviour to session variables, because one unhappy named session variable can breaks an applications. The problems related to badly named PLpgSQL's varible is limitted just to only one routine. --- doc/src/sgml/config.sgml | 60 +++++++++++++++++++ doc/src/sgml/ddl.sgml | 10 ++++ src/backend/parser/parse_expr.c | 49 +++++++++++++-- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/parser/parse_expr.h | 1 + .../src/expected/plpgsql_session_variable.out | 33 ++++++++++ .../src/sql/plpgsql_session_variable.sql | 29 +++++++++ .../regress/expected/session_variables.out | 22 +++++++ src/test/regress/sql/session_variables.sql | 20 +++++++ 10 files changed, 230 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a84e60c09b9..d1b2175c022 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10740,6 +10740,66 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir' </listitem> </varlistentry> + <varlistentry id="guc-session-variables-ambiguity-warning" xreflabel="session_variables_ambiguity_warning"> + <term><varname>session_variables_ambiguity_warning</varname> (<type>boolean</type>) + <indexterm> + <primary><varname>session_variables_ambiguity_warning</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + When on, a warning is raised when any identifier in a query could be + used as both a column identifier, routine variable or a session + variable identifier. The default is <literal>off</literal>. + </para> + <para> + Session variables can be shadowed by column references in a query, this + is an expected behavior. Previously working queries shouldn't error out + by creating any session variable, so session variables are always shadowed + if an identifier is ambiguous. Variables should be referenced using + anunambiguous identifier without any possibility for a collision with + identifier of other database objects (column names or record fields names). + The warning messages emitted when enabling <varname>session_variables_ambiguity_warning</varname> + can help finding such identifier collision. +<programlisting> +CREATE TABLE foo(a int); +INSERT INTO foo VALUES(10); +CREATE VARIABLE a int; +LET a = 100; +SELECT a FROM foo; +</programlisting> + +<screen> + a +---- + 10 +(1 row) +</screen> + +<programlisting> +SET session_variables_ambiguity_warning TO on; +SELECT a FROM foo; +</programlisting> + +<screen> +WARNING: session variable "a" is shadowed +LINE 1: SELECT a FROM foo; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a +---- + 10 +(1 row) +</screen> + </para> + <para> + This feature can significantly increase log size, so it's disabled by + default. For testing or development environments it's recommended to + enable it if you use session variables. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-standard-conforming-strings" xreflabel="standard_conforming_strings"> <term><varname>standard_conforming_strings</varname> (<type>boolean</type>) <indexterm><primary>strings</primary><secondary>standard conforming</secondary></indexterm> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 327548fae12..ce1b1e1f599 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5359,6 +5359,16 @@ SELECT current_user_id; the schema name, columns can use table aliases, routine variables can use block labels, and routine arguments can use the routine name. </para> + + <para> + When a query contains identifiers or qualified identifiers that could be + used as both a session variable identifiers and as column identifier, + then the column identifier is preferred every time. Warnings can be + emitted when this situation happens by enabling configuration parameter <xref + linkend="guc-session-variables-ambiguity-warning"/>. User can explicitly + qualify the source object by syntax <literal>table.column</literal> or + <literal>schema.variable</literal>. + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index aba5259e027..3602c3572d7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -45,6 +45,7 @@ /* GUC parameters */ bool Transform_null_equals = false; +bool session_variables_ambiguity_warning = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -960,11 +961,51 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) * * SELECT foo.a, foo.b, foo.c FROM foo; * - * However, that is very confusing, so we disallow it. We don't try to - * identify a variable if we know that it would be shadowed. - * ----- + * However, that is very confusing, so we disallow it. + * + * When session_variables_ambiguity_warning is requested, then we + * need to identify a variable although we know, so this variable + * would be shadowed. */ - if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + if (node || (relname && crerr == CRERR_NO_COLUMN)) + { + /* + * In this path we just try (if it is wanted) detect if session + * variable is shadowed. + */ + if (session_variables_ambiguity_warning) + { + /* + * The AccessShareLock is created on related session variable. + * The lock will be kept for the whole transaction. + */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, true); + + if (OidIsValid(varid)) + { + /* this path will ending by WARNING. Unlock variable first */ + UnlockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (node) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is shadowed", + NameListToString(cref->fields)), + errdetail("Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name."), + parser_errposition(pstate, cref->location))); + else + /* session variable is shadowed by RTE */ + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s.%s\" is shadowed", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("Session variables can be shadowed by tables or table's aliases with the same name."), + parser_errposition(pstate, cref->location))); + } + } + } + else { /* takes an AccessShareLock on the session variable */ varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 8a67f01200c..efb65b02380 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1595,6 +1595,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_ambiguity_warning", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when reference to a session variable is ambiguous."), + NULL + }, + &session_variables_ambiguity_warning, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 39a3ac23127..992dda3c644 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -714,6 +714,7 @@ #default_transaction_read_only = off #default_transaction_deferrable = off #session_replication_role = 'origin' +#session_variables_ambiguity_warning = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 9b46dfd9ecc..0d64b300f8a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -17,6 +17,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; +extern PGDLLIMPORT bool session_variables_ambiguity_warning; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ab779900596..a6523f7afe2 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -354,6 +354,39 @@ $$; NOTICE: session variable is 100 NOTICE: plpgsql variable is 1000 NOTICE: variable is 1000 +-- againt with session_variables_ambiguity_warning(on) +SET session_variables_ambiguity_warning TO on; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +WARNING: session variable "plpgsql_sv_var1" is shadowed +LINE 1: plpgsql_sv_var1 + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. +QUERY: plpgsql_sv_var1 +NOTICE: variable is 1000 +SET session_variables_ambiguity_warning TO off; DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted CREATE VARIABLE plpgsql_sv_v text; diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql index a3cc264cf38..9b8e90e81fa 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -260,6 +260,35 @@ BEGIN END; $$; +-- againt with session_variables_ambiguity_warning(on) + +SET session_variables_ambiguity_warning TO on; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +SET session_variables_ambiguity_warning TO off; + DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 85771032de3..50fb478b5e9 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1261,3 +1261,25 @@ SELECT var1; (1 row) DROP VARIABLE var1, var2; +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; +CREATE VARIABLE xxtab.avar int; +CREATE TABLE public.xxtab(avar int); +INSERT INTO public.xxtab VALUES(1); +LET xxtab.avar = 20; +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; +WARNING: session variable "xxtab.avar" is shadowed +LINE 1: SELECT xxtab.avar FROM public.xxtab; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + avar +------ + 1 +(1 row) + +SET session_variables_ambiguity_warning TO off; +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; +NOTICE: drop cascades to session variable xxtab.avar diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index d8397573eeb..e7a4fdedae4 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -854,3 +854,23 @@ BEGIN; DROP VARIABLE var1; ROLLBACK; SELECT var1; DROP VARIABLE var1, var2; + +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; + +CREATE VARIABLE xxtab.avar int; + +CREATE TABLE public.xxtab(avar int); + +INSERT INTO public.xxtab VALUES(1); + +LET xxtab.avar = 20; + +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; + +SET session_variables_ambiguity_warning TO off; + +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; -- 2.47.0 [text/x-patch] v20241119-2-0005-memory-cleaning-after-DROP-VARIABLE.patch (20.8K, 20-v20241119-2-0005-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From ce9412459d35712a5abb37f6e7a8520641125940 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 09:31:28 +0100 Subject: [PATCH 05/22] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 153 +++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../regress/expected/session_variables.out | 217 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 120 ++++++++++ 8 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index ef89594a268..c7369e2b2ac 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/builtins.h" @@ -254,4 +255,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 668b8189a6c..21c7a9517b5 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" @@ -67,6 +68,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -83,6 +92,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -114,6 +134,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -167,6 +219,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -196,6 +309,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -319,22 +434,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -395,6 +530,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 443afbafd4a..3dab8ae2a48 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..0a5579dc7ce --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 143109aa4da..7453685d046 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -115,3 +115,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..c864fee4006 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT myvar; } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2c4760fb687..85771032de3 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1044,3 +1044,220 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; +-- should fail +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + Ahoj +(1 row) + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + var1 +------ + Ahoj +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1, var2; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 979b9029e58..d8397573eeb 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -734,3 +734,123 @@ DISCARD VARIABLES; -- should be zero again SELECT count(*) FROM pg_session_variables(); + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; + +-- should fail +SELECT var1; + +ROLLBACK; + +-- should be ok +SELECT var1; + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; +ROLLBACK; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; +COMMIT; +-- should be ok +SELECT var1; + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1, var2; -- 2.47.0 [text/x-patch] v20241119-2-0004-DISCARD-VARIABLES.patch (9.6K, 21-v20241119-2-0004-DISCARD-VARIABLES.patch) download | inline diff: From 86c18d7defbe281e51a42cb0f190d265a5b6b5be Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 28 Jan 2024 20:54:20 +0100 Subject: [PATCH 04/22] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 +++- src/backend/commands/discard.c | 6 ++ src/backend/commands/session_variable.c | 28 ++++++++- src/backend/parser/gram.y | 6 ++ src/backend/tcop/utility.c | 3 + src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../regress/expected/session_variables.out | 63 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 36 +++++++++++ 11 files changed, 158 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 92d983ac748..d06ebaba6f4 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 13ab9575fa9..668b8189a6c 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -94,7 +94,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -661,3 +667,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8ae368c513c..aad47305f20 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2077,7 +2077,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 69d2f7e1ced..970451460c2 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2962,6 +2962,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec572815c94..3ef4776d98a 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4083,7 +4083,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index b3f03c65827..443afbafd4a 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expect extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 778db6ad4f2..04ab22bd492 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3988,6 +3988,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index a921af2486f..bd7964aea6e 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a2cb35e3d7d..2c4760fb687 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -981,3 +981,66 @@ DROP VARIABLE :"DBNAME".:"DBNAME".b; DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + var1 +------- + Hello +(1 row) + +DISCARD ALL; +SELECT var1; + var1 +------ + +(1 row) + +LET var1 = 'AHOJ'; +SELECT var1; + var1 +------ + AHOJ +(1 row) + +DISCARD VARIABLES; +SELECT var1; + var1 +------ + +(1 row) + +DROP VARIABLE var1; +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS varchar; +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +LET var1 = 'AHOJ'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DISCARD VARIABLES; +-- should be zero again +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 6445479d22d..979b9029e58 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -698,3 +698,39 @@ DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; + +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + +DISCARD ALL; +SELECT var1; + +LET var1 = 'AHOJ'; +SELECT var1; + +DISCARD VARIABLES; +SELECT var1; + +DROP VARIABLE var1; + +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS varchar; + +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + +LET var1 = 'AHOJ'; + +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + +DISCARD VARIABLES; + +-- should be zero again +SELECT count(*) FROM pg_session_variables(); -- 2.47.0 [text/x-patch] v20241119-2-0003-function-pg_session_variables-for-cleaning-tests.patch (4.3K, 22-v20241119-2-0003-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From c2036e9ca0936a31623e8dbe05892d3af3ea5010 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 03/22] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 92 +++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 +++ 2 files changed, 100 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 548d9835c0d..13ab9575fa9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -569,3 +569,95 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 41f1121d19f..e943a74846f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12405,4 +12405,12 @@ proargtypes => 'int2', prosrc => 'gist_stratnum_identity' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.47.0 [text/x-patch] v20241119-2-0002-Storage-for-session-variables-and-SQL-interface.patch (145.2K, 23-v20241119-2-0002-Storage-for-session-variables-and-SQL-interface.patch) download | inline diff: From 9378625d8f2ac2c22cd642d9d92a1025846af9df Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Thu, 6 Jul 2023 08:29:21 +0200 Subject: [PATCH 02/22] Storage for session variables and SQL interface Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. The identifiers of session variables should always be shadowed by possible column identifiers: we don't want to break an application by creating some badly named session variable. The limits of this patch (solved by other patches): - session variables block parallel execution - session variables blocks simple expression evaluation (in plpgsql) - SQL functions with session variables are not inlined - CALL statement is not supported (usage of direct access to express executor) - EXECUTE statement is not supported (usage of direct access to express executor) - memory used by dropped session variables is not released Implementations of EXPLAIN LET and PREPARE LET statements are in separate patches (for better readability) --- doc/src/sgml/catalogs.sgml | 13 + doc/src/sgml/ddl.sgml | 32 + doc/src/sgml/parallel.sgml | 6 + doc/src/sgml/plpgsql.sgml | 14 + doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 1 + doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++ doc/src/sgml/reference.sgml | 1 + src/backend/catalog/dependency.c | 5 + src/backend/catalog/namespace.c | 315 ++++++ src/backend/catalog/pg_variable.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/prepare.c | 8 + src/backend/commands/session_variable.c | 571 +++++++++++ src/backend/executor/Makefile | 1 + src/backend/executor/execExpr.c | 36 + src/backend/executor/execMain.c | 57 ++ src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 201 ++++ src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 9 + src/backend/optimizer/plan/setrefs.c | 135 ++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 35 +- src/backend/parser/analyze.c | 271 ++++- src/backend/parser/gram.y | 49 +- src/backend/parser/parse_agg.c | 9 + src/backend/parser/parse_expr.c | 220 ++++- src/backend/parser/parse_func.c | 2 + src/backend/tcop/dest.c | 7 + src/backend/tcop/pquery.c | 3 + src/backend/tcop/utility.c | 16 + src/backend/utils/adt/ruleutils.c | 46 + src/backend/utils/cache/plancache.c | 41 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/namespace.h | 2 + src/include/catalog/pg_variable.h | 8 + src/include/commands/session_variable.h | 32 + src/include/executor/execdesc.h | 4 + src/include/executor/svariableReceiver.h | 22 + src/include/nodes/execnodes.h | 16 + src/include/nodes/parsenodes.h | 17 + src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 4 +- src/include/nodes/primnodes.h | 6 + src/include/optimizer/planmain.h | 2 + src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 3 + src/include/tcop/cmdtaglist.h | 1 + src/include/tcop/dest.h | 1 + src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 925 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 639 ++++++++++++ src/tools/pgindent/typedefs.list | 5 + 58 files changed, 3916 insertions(+), 23 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/include/executor/svariableReceiver.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bdd2ca0152f..0c15d56dd42 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9785,6 +9785,19 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>XLogRecPtr</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varname</structfield> <type>name</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2e3f0b95fb0..327548fae12 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5317,6 +5317,38 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; VARIABLE</command> command. </para> + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT READ ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); +SELECT current_user_id; +</programlisting> + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> + <para> Inside a query or an expression, a session variable can be <quote>shadowed</quote> by a column with the same name. Similarly, the diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 78e4983139b..bfbff9ab74e 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6034,6 +6034,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index d87570e7d8b..d2036351e53 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index ec165ab96fc..05bf67e3411 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -145,6 +145,7 @@ SELECT var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..f6640576bee --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>session_variable</literal></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>sql_expression</literal></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + + <para> + Example: +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c9d686665de..148719e490e 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 86e2258657d..487353ef2eb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3412,6 +3412,321 @@ LookupVariable(const char *nspname, return varoid; } +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} + +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or "variable"."field". + * Check both variants, and returns InvalidOid with + * not_unique flag, when both interpretations are + * possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * In this case, the field1 should be variable name. But + * direct unboxing of composite session variables is not + * supported now, and then we don't need to try lookup + * related variable. + * + * Unboxing is supported by syntax (var).* + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.field. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else if (OidIsValid(varid)) + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index e3a861c8cba..ef89594a268 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -26,6 +26,7 @@ #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/pg_lsn.h" #include "utils/syscache.h" static ObjectAddress create_variable(const char *varName, @@ -101,6 +102,7 @@ create_variable(const char *varName, varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 48f7348f91c..1cfaeca51ea 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -50,6 +50,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 6dd00a4abde..ca621be5ecb 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -38,6 +38,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index a93f970a292..455a03cce63 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,6 +338,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..548d9835c0d --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,571 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "rewrite/rewriteHandler.h" +#include "storage/lmgr.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Wrapper around SetSessionVariable with permission checks. + */ +void +SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull) +{ + AclResult aclresult; + + /* is the caller allowed to update the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + SetSessionVariable(varid, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull, Oid *typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message ws processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + Assert(svar); + + *typid = svar->typid; + + return copy_session_variable_value(svar, isNull); +} + +/* + * Returns a copy of the value of the session variable after checking if the + * type is the same as "expected_typid". The caller is responsible for + * permission checks. + */ +Datum +GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + Assert(svar && svar->is_valid); + + if (expected_typid != svar->typid) + elog(ERROR, "type of variable \"%s.%s\" is different than expected", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)); + + return copy_session_variable_value(svar, isNull); +} + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L, true); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 8f7a5340059..6dc8c562bec 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -34,6 +34,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -991,6 +992,41 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.d.param.paramtype = param->paramtype; ExprEvalPushStep(state, &scratch); break; + + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; + case PARAM_EXTERN: /* diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 5ca856fd279..21e938a6227 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/nodeSubplan.h" @@ -195,6 +197,61 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + ListCell *lc; + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach(lc, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + Oid varid = lfirst_oid(lc); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].varid = varid; + estate->es_session_variables[i].value = GetSessionVariable(varid, + &estate->es_session_variables[i].isnull, + &estate->es_session_variables[i].typid); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index b511a429adf..b6662c6e8fc 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c4cff44ecf4 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,201 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. Because a received tuple can have + * deleted attributes, we need to find the first non-deleted attribute + * (field "slot_offset"). The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int slot_offset; /* position of non-deleted attribute */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + int natts = typeinfo->natts; + int outcols = 0; + int i; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + for (i = 0; i < natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(typeinfo, i); + Oid typid; + Oid collid; + int32 typmod; + + if (attr->attisdropped) + continue; + + if (++outcols > 1) + continue; + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + /* + * Double check - the type and typmod of target variable should be the + * same as the type and typmod of assignment expression. The + * expression should be wrapped by a cast to the target type/typmod. + */ + if (attr->atttypid != typid || + (attr->atttypmod >= 0 && + attr->atttypmod != typmod)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("target session variable is of type %s" + " but expression is of type %s", + format_type_with_typemod(typid, typmod), + format_type_with_typemod(attr->atttypid, + attr->atttypmod)))); + + myState->need_detoast = attr->attlen == -1; + myState->slot_offset = i; + } + + if (outcols != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + outcols, + outcols))); + + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[myState->slot_offset]; + isnull = slot->tts_isnull[myState->slot_offset]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3060847b133..8fc67f08d76 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4341,6 +4341,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 1f78dc3d530..aee3a0d1ad4 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -325,6 +325,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->lastPlanNodeId = 0; glob->transientPlan = false; glob->dependsOnRole = false; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -559,6 +560,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + result->sessionVariables = glob->sessionVariables; result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -729,6 +731,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 91c7c4fe2fe..1fa2ecf5c20 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1310,6 +1313,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -1958,8 +2005,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2049,6 +2097,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2058,6 +2113,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2076,6 +2135,41 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + ListCell *lc; + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach(lc, root->glob->sessionVariables) + { + if (lfirst_oid(lc) == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2137,7 +2231,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2159,7 +2256,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3544,6 +3642,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list, or as the target of a LET statement. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries @@ -3629,9 +3746,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3654,6 +3771,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, (void *) context, 0); diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 33b10d72cb5..9f698a46d87 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1410,6 +1410,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8e39795e245..a5452c0c9e4 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -935,6 +936,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2389,6 +2397,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2515,6 +2524,29 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariableWithTypeCheck(param->paramvarid, + &isnull, + param->paramtype); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4718,7 +4750,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 506e0631615..81b8b0a633d 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,9 +25,12 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -51,6 +54,7 @@ #include "utils/backend_status.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -83,6 +87,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -414,6 +420,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -493,6 +500,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -545,6 +557,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -653,6 +666,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1079,6 +1093,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1544,6 +1559,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1770,12 +1786,242 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + AclResult aclresult; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type", + format_type_be(typid), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, stmt->location))); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names)); + + pstate->p_expr_kind = EXPR_KIND_LET_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach(lc, selectQuery->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Param *param; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s," + " but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. This value is used multiple times: by + * the query rewriter (for getting related defexpr), by planner (for + * acquiring an AccessShareLock on target variable), and by the executor + * (we need to know the target variable ID). + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * transformSetOperationStmt - * transforms a set-operations tree @@ -2021,6 +2267,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2496,6 +2743,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2563,6 +2811,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2750,9 +2999,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Transform the target reference. Typically we will get back a Param * node, but there's no reason to be too picky about its type. + * + * Session variables should not be used as target of a PL/pgSQL assign + * statement. So we should use a dedicated expression kind and disallow + * session variables there. The dedicated context allows to eliminate + * undesirable warnings about the possibility of a target PL/pgSQL variable + * shadowing a session variable. */ target = transformExpr(pstate, (Node *) cref, - EXPR_KIND_UPDATE_TARGET); + EXPR_KIND_ASSIGN_TARGET); targettype = exprType(target); targettypmod = exprTypmod(target); targetcollation = exprCollation(target); @@ -2794,6 +3049,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) */ type_id = exprType((Node *) tle->expr); + /* + * For indirection processing and additional casts we can use expr_kind + * EXPR_KIND_UPDATE_TARGET. + */ pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; if (indirection) @@ -2936,6 +3195,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ @@ -3209,6 +3470,14 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); + /* + * The arguments of CALL statement are evaluated by a direct expression + * executor call. This path is unsupported yet, so block it. It will be + * enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 89fa23c2dd4..8ae368c513c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -293,7 +293,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -443,6 +443,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list + let_target %type <node> opt_routine_body %type <groupclause> group_clause @@ -734,7 +735,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1081,6 +1082,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12770,6 +12772,47 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET let_target '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = $2; + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + +let_target: + ColId opt_indirection + { + $$ = list_make1(makeString($1)); + if ($2) + $$ = list_concat($$, + check_indirection($2, yyscanner)); + } + ; + /***************************************************************************** * * QUERY: @@ -17840,6 +17883,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18451,6 +18495,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index efa730c1676..a7343001f21 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -580,6 +580,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -970,6 +975,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c2806297aa4..aba5259e027 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -17,6 +17,7 @@ #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -33,11 +34,13 @@ #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" /* GUC parameters */ @@ -106,6 +109,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* @@ -499,6 +505,89 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_POLICY: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_COPY_WHERE: + case EXPR_KIND_LET_TARGET: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + result = false; + break; + } + + return result; +} + /* * Transform a ColumnRef. * @@ -575,6 +664,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: /* okay */ break; @@ -847,8 +938,61 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) } /* - * Throw error if no translation found. + * There are contexts where session variables are not allowed. We don't + * need to identify session variables in such a context, but identifying + * them allows us to raise meaningful error messages like "you cannot use + * session variables here". */ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + { + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* ----- + * Session variables are shadowed by columns, routine variables or + * routine arguments. We certainly don't want to use a session variable + * when it is exactly shadowed, but a RTE like this is conceivable: + * + * CREATE TYPE t AS (c int); + * CREATE VARIABLE foo AS t; + * CREATE TABLE foo(a int, b int); + * + * SELECT foo.a, foo.b, foo.c FROM foo; + * + * However, that is very confusing, so we disallow it. We don't try to + * identify a variable if we know that it would be shadowed. + * ----- + */ + if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + { + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + node = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, cref->location); + } + } + } + + /* throw an error if no translation was found */ if (node == NULL) { switch (crerr) @@ -880,6 +1024,72 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable", attrname), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + static Node * transformParamRef(ParseState *pstate, ParamRef *pref) { @@ -1815,6 +2025,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_LET_TARGET: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -1858,6 +2069,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_ASSIGN_TARGET: + err = _("cannot use subquery as target of assign statement"); + break; /* * There is intentionally no default: case here, so that the @@ -3197,6 +3411,10 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_ASSIGN_TARGET: + return "ASSIGN"; + case EXPR_KIND_LET_TARGET: + return "LET"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9b23344a3b1..9aa4f60768b 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2656,6 +2656,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) err = _("set-returning functions are not allowed in column generation expressions"); break; case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: errkind = true; break; diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index 96f80b30463..dac78ba5cce 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 0c45fcf318f..85b809cb071 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -86,6 +86,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt, qd->queryEnv = queryEnv; qd->instrument_options = instrument_options; /* instrumentation wanted? */ + qd->num_session_variables = 0; + qd->session_variables = NULL; + /* null these fields until set by ExecutorStart */ qd->tupDesc = NULL; qd->estate = NULL; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 217c6028497..69d2f7e1ced 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -49,6 +49,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -235,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1069,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2213,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2415,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3304,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a39068d1bf1..e7f74947cde 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -528,6 +529,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8617,6 +8619,14 @@ get_parameter(Param *param, deparse_context *context) return; } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Alternatively, maybe it's a subplan output, which we print as a * reference to the subplan. (We could drill down into the subplan and @@ -13399,6 +13409,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 5af1a168ec2..7e1e9e28f12 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -162,6 +163,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -1903,18 +1905,33 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, (void *) &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -1929,6 +1946,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already @@ -2060,7 +2091,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index e48a86be54b..eaae0c5a933 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 293648e30c7..ec572815c94 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1214,8 +1214,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4629,6 +4629,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 18ea6ac83a5..def691dafb3 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -171,7 +171,9 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror); extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 3810e040f28..db593cd5bed 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -35,6 +35,14 @@ CATALOG(pg_variable,9222,VariableRelationId) /* OID of entry in pg_type for variable's type */ Oid vartype BKI_LOOKUP(pg_type); + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + /* variable name */ NameData varname; diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..b3f03c65827 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,32 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "nodes/params.h" +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" +#include "tcop/cmdtag.h" +#include "utils/queryenvironment.h" + +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); +extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid); + +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + +#endif diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h index 0a7274e26c8..d1b2f59e0cf 100644 --- a/src/include/executor/execdesc.h +++ b/src/include/executor/execdesc.h @@ -48,6 +48,10 @@ typedef struct QueryDesc EState *estate; /* executor's query-wide state */ PlanState *planstate; /* tree of per-plan-node state */ + /* reference to session variables buffer */ + int num_session_variables; + SessionVariableValue *session_variables; + /* This field is set by ExecutorRun */ bool already_executed; /* true if previously executed */ diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 182a6956bb0..b4464edc22a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -617,6 +617,18 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + Oid varid; + Oid typid; + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -669,6 +681,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0e08296b438..778db6ad4f2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -142,6 +142,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -162,6 +165,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -2100,6 +2105,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + int location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index add0f9e45fc..ea1f76d0fbb 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -163,6 +163,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -508,6 +511,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 52f29bcdb69..4549366cf59 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -49,7 +49,7 @@ typedef struct PlannedStmt NodeTag type; - CmdType commandType; /* select|insert|update|delete|merge|utility */ + CmdType commandType; /* select|let|insert|update|delete|merge|utility */ uint64 queryId; /* query identifier (copied from Query) */ @@ -94,6 +94,8 @@ typedef struct PlannedStmt Node *utilityStmt; /* non-null if this is utility stmt */ + List *sessionVariables; /* OIDs for PARAM_VARIABLE Params */ + /* statement location in source string (copied from Query) */ ParseLoc stmt_location; /* start location, or -1 if unknown */ ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index b0ef1952e8f..4b2424d6d34 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -361,6 +361,9 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramid holds the variable's OID). */ typedef enum ParamKind { @@ -368,6 +371,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -380,6 +384,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of session variable if it is used */ + Oid paramvarid; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 93137261e48..61341c50cf7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -124,4 +124,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 56670e65b11..aefe51f335b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -256,6 +256,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 2375e95c107..c11fdb3d970 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -82,6 +82,8 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ + EXPR_KIND_LET_TARGET, /* LET target */ } ParseExprKind; @@ -244,6 +246,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 9465df7b2ff..a921af2486f 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index a3d521b6f97..38cc87f4e7a 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..1361a8a8480 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,7 +8099,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index d0aff56a01d..a2cb35e3d7d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -56,3 +56,928 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; +-- check access rights +CREATE ROLE regress_noowner; +CREATE VARIABLE var1 AS int; +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; +LET var1 = 10; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +-- should fail +SET ROLE TO regress_noowner; +SELECT var1; +ERROR: permission denied for session variable var1 +SELECT sqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: SQL function "sqlfx" statement 1 +SELECT plpgsqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "$1 + var1" +PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +-- should be ok +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; +-- should be ok +SET ROLE TO regress_noowner; +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); +DROP ROLE regress_noowner; +-- use variables inside views +CREATE VARIABLE var1 AS numeric; +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- start a new session +\c +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- should fail, dependency +DROP VARIABLE var1; +ERROR: cannot drop session variable var1 because other objects depend on it +DETAIL: view test_view depends on session variable var1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view test_view +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; +LET var1 = 'str1'; +LET var2 = 'str2'; +SELECT sqlfx1(sqlfx2('Hello')); + sqlfx1 +------------------- + str1, str2, Hello +(1 row) + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + QUERY PLAN +------------------------------------------------------ + Result + Output: sqlfx1(sqlfx2('Hello'::character varying)) +(2 rows) + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; +set plan_cache_mode TO force_generic_plan; +LET var1 = 3.14; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 3.14 +(1 row) + +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 6.28 +(1 row) + +DROP VARIABLE var1; +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 9.42 +(1 row) + +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 12.56 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); +set plan_cache_mode TO DEFAULT; +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; +NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} +DROP VARIABLE var1; +DROP VARIABLE var2; +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; +-- should not crash (but is not supported yet) +CALL p(v); +ERROR: session variable cannot be used as an argument +DO $$ BEGIN CALL p(v); END $$; +ERROR: session variable cannot be used as an argument +CONTEXT: SQL statement "CALL p(v)" +PL/pgSQL function inline_code_block line 1 at CALL +DROP PROCEDURE p(int); +DROP VARIABLE v; +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; +-- should fail +EXECUTE ptest(v); +ERROR: session variable cannot be used as an argument +DEALLOCATE ptest; +DROP VARIABLE v; +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; +-- should fail +LET var1 = pi(); +ERROR: session variable "var1" doesn't exist +LINE 1: LET var1 = pi(); + ^ +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + var1 +------------------ + 3.14159265358979 +(1 row) + +SET search_path TO svartest; +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + var1 +------------------ + 13.1415926535898 +(1 row) + +RESET search_path; +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to session variable svartest.var1 +CREATE VARIABLE var1 AS text; +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; +SELECT var1; + var1 +------- + hello +(1 row) + +DROP VARIABLE var1; +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; +-- should fail +SELECT var1; +ERROR: domain int_domain does not allow null values +-- should be ok +LET var1 = 1000; +SELECT var1; + var1 +------ + 1000 +(1 row) + +-- should fail +LET var1 = 10; +ERROR: value for domain int_domain violates check constraint "int_domain_check" +-- should fail +LET var1 = NULL; +ERROR: domain int_domain does not allow null values +-- note - domain defaults are not supported yet (like PLpgSQL) +DROP VARIABLE var1; +DROP DOMAIN int_domain; +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + var1 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +DROP VARIABLE var1; +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + var1 +------ + 100 +(1 row) + +SET search_path to public, svartest; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table foo +drop cascades to session variable var1 +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +LET var2[var1] = 0; +SELECT var2; + var2 +----------- + [2:2]={0} +(1 row) + +DROP VARIABLE var1, var2; +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +SELECT var2; + var2 +------ + {} +(1 row) + +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" +LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x; + ^ +-- should be ok +LET var1 = generate_series(1, 1); +-- should fail +LET var1 = generate_series(1, 2); +ERROR: expression returned more than one row +LET var1 = generate_series(1, 0); +ERROR: expression returned no rows +DROP VARIABLE var1; +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); +SELECT v1; + v1 +------------ + (1,2,3.00) +(1 row) + +SELECT v2; + v2 +--------------- + (10,20,31.40) +(1 row) + +SELECT (v1).*; + x | y | z +---+---+------ + 1 | 2 | 3.00 +(1 row) + +SELECT (v2).*; + x | y | z +----+----+------- + 10 | 20 | 31.40 +(1 row) + +SELECT v1.x + v1.z; + ?column? +---------- + 4.00 +(1 row) + +SELECT v2.x + v2.z; + ?column? +---------- + 41.40 +(1 row) + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should fail +SELECT v2.x; +ERROR: permission denied for session variable v2 +SET ROLE TO DEFAULT; +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; +CREATE TYPE t1 AS (a int, b numeric, c text); +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; + v1 +---------------------------- + (1,3.14159265358979,hello) +(1 row) + +LET v1.b = 10.2222; +SELECT v1; + v1 +------------------- + (1,10.2222,hello) +(1 row) + +-- should fail, attribute doesn't exist +LET v1.x = 10; +ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +LINE 1: LET v1.x = 10; + ^ +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; +ERROR: assignment expression returned 3 columns +LINE 1: LET v1 = (NULL::t1).*; + ^ +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + v1 +------------- + (1,10.2222) +(1 row) + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + v1 +-------------- + (1,10.2222,) +(1 row) + +LET v1 = (10, 10.3, 20); +SELECT v1; + v1 +-------------- + (10,10.3,20) +(1 row) + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + v1 +--------- + (10,20) +(1 row) + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; +ERROR: cannot alter type "t1" because session variable "public.v1" uses it +DROP VARIABLE v1; +DROP TYPE t1; +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + var1 +---------------------------------- + (10,3.14159265358979,05-26-2023) +(1 row) + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; +ERROR: cannot alter table "svar_test" because session variable "public.var1" uses it +-- should fail +DROP TABLE svar_test; +ERROR: cannot drop table svar_test because other objects depend on it +DETAIL: session variable var1 depends on type svar_test +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VARIABLE var1; +DROP TABLE svar_test; +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + var1 +------------ + {10.1,2.1} +(1 row) + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; +ERROR: set-returning functions are not allowed in LET +LINE 1: LET var1[generate_series(1,3)] = 100; + ^ +DROP VARIABLE var1; +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; + var1 +-------------------- + (10.2,"{0.0,0.0}") +(1 row) + +LET var1.b[1] = 10.3; +SELECT var1; + var1 +--------------------- + (10.2,"{10.3,0.0}") +(1 row) + +DROP VARIABLE var1; +DROP TYPE t1; +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + count +------- + 100 +(1 row) + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + QUERY PLAN +----------------------------------- + Aggregate + -> Seq Scan on svar_test + Filter: ((a % 10) = zero) +(3 rows) + +LET zero = (SELECT count(*) FROM svar_test); +-- result should be 1000 +SELECT zero; + zero +------ + 1000 +(1 row) + +DROP VARIABLE zero; +DROP TABLE svar_test; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO on; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO off; +DROP VIEW var1view; +DROP VARIABLE var1; +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; + id | v +----+----- + 1 | 100 +(1 row) + +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+----- + 1 | 200 +(1 row) + +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+--- +(0 rows) + +DROP TABLE svar_test; +DROP VARIABLE varid; +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + relname +---------- + pg_class +(1 row) + +DROP VARIABLE var1; +CREATE TABLE xxtab(avar int); +INSERT INTO xxtab VALUES(333); +CREATE TYPE xxtype AS (avar int); +CREATE VARIABLE xxtab AS xxtype; +INSERT INTO xxtab VALUES(10); +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +CREATE VARIABLE public.avar AS int; +-- should be ok, see the table +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT public.avar FROM xxtab; + avar +------ + + +(2 rows) + +DROP VARIABLE xxtab; +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +DROP VARIABLE public.avar; +DROP TYPE xxtype; +DROP TABLE xxtab; +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; +CREATE TABLE public.svar(a int, b int); +INSERT INTO public.svar VALUES(10, 20); +LET public.svar = (100, 200, 300); +-- should be ok +-- show table +SELECT * FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; +CREATE TYPE ab AS (a integer, b integer); +CREATE VARIABLE v_ab AS ab; +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +CREATE SCHEMA v_ab; +CREATE VARIABLE v_ab.a AS integer; +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; +SET search_path TO public; +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); +-- should be ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; +-- should be still ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; +ERROR: session variable reference "xxx_am.b" is ambiguous +LINE 1: SELECT xxx_am.b; + ^ +-- enhanced references should be ok +SELECT public.xxx_am.b; + b +---- + 10 +(1 row) + +SELECT :"DBNAME".xxx_am.b; + b +---- + 20 +(1 row) + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); +-- we should see table +SELECT xxx_am.b FROM xxx_am; + b +---- + 10 +(1 row) + +SELECT x.b FROM xxx_am x; + b +---- + 10 +(1 row) + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; +CREATE SCHEMA :"DBNAME"; +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; +SET search_path TO :"DBNAME"; +-- should be ambiguous +SELECT :"DBNAME".b; +ERROR: session variable reference "regression.b" is ambiguous +LINE 1: SELECT "regression".b; + ^ +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; +ERROR: session variable reference "regression.regression.b" is ambiguous +LINE 1: SELECT "regression"."regression".b; + ^ +CREATE TABLE :"DBNAME"(b int); +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + b +--- +(0 rows) + +DROP TABLE :"DBNAME"; +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; +RESET search_path; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 82679cbf48e..6445479d22d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -59,3 +59,642 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; + +-- check access rights +CREATE ROLE regress_noowner; + +CREATE VARIABLE var1 AS int; + +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; + +LET var1 = 10; +-- should be ok +SELECT var1; +SELECT sqlfx(20); +SELECT sqlfx_sd(20); +SELECT plpgsqlfx(20); +SELECT plpgsqlfx_sd(20); + +-- should fail +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +-- should be ok +SELECT sqlfx_sd(20); +SELECT plpgsqlfx_sd(20); + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; + +-- should be ok +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); + +DROP ROLE regress_noowner; + +-- use variables inside views +CREATE VARIABLE var1 AS numeric; + +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- start a new session +\c + +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- should fail, dependency +DROP VARIABLE var1; + +-- should be ok +DROP VARIABLE var1 CASCADE; + +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; + +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; + +LET var1 = 'str1'; +LET var2 = 'str2'; + +SELECT sqlfx1(sqlfx2('Hello')); + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; + +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; + +set plan_cache_mode TO force_generic_plan; + +LET var1 = 3.14; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; + +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; + +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); + +set plan_cache_mode TO DEFAULT; + +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; + +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; + +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; + +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; + +-- should not crash (but is not supported yet) +CALL p(v); + +DO $$ BEGIN CALL p(v); END $$; + +DROP PROCEDURE p(int); +DROP VARIABLE v; + +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; + +-- should fail +EXECUTE ptest(v); + +DEALLOCATE ptest; +DROP VARIABLE v; + +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; + +-- should fail +LET var1 = pi(); +SELECT var1; + +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + +SET search_path TO svartest; + +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + +RESET search_path; +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS text; + +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; + +SELECT var1; + +DROP VARIABLE var1; + +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; + +-- should fail +SELECT var1; + +-- should be ok +LET var1 = 1000; +SELECT var1; + +-- should fail +LET var1 = 10; + +-- should fail +LET var1 = NULL; + +-- note - domain defaults are not supported yet (like PLpgSQL) + +DROP VARIABLE var1; +DROP DOMAIN int_domain; + +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; + +DROP VARIABLE var1; + +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + +SET search_path to public, svartest; + +SELECT var1; + +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +LET var2[var1] = 0; + +SELECT var2; + +DROP VARIABLE var1, var2; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +SELECT var2; + +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; + +-- should be ok +LET var1 = generate_series(1, 1); + +-- should fail +LET var1 = generate_series(1, 2); +LET var1 = generate_series(1, 0); + +DROP VARIABLE var1; + +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); + +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; + +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); + +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); + +SELECT v1; +SELECT v2; +SELECT (v1).*; +SELECT (v2).*; + +SELECT v1.x + v1.z; +SELECT v2.x + v2.z; + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; + +SET ROLE TO regress_var_test_role; + +-- should fail +SELECT v2.x; + +SET ROLE TO DEFAULT; + +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; + +CREATE TYPE t1 AS (a int, b numeric, c text); + +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; +LET v1.b = 10.2222; +SELECT v1; + +-- should fail, attribute doesn't exist +LET v1.x = 10; + +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; + +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + +LET v1 = (10, 10.3, 20); +SELECT v1; + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; + +DROP VARIABLE v1; +DROP TYPE t1; + +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; + +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; + +-- should fail +DROP TABLE svar_test; + +DROP VARIABLE var1; +DROP TABLE svar_test; + +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; + +DROP VARIABLE var1; + +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; +LET var1.b[1] = 10.3; +SELECT var1; + +DROP VARIABLE var1; +DROP TYPE t1; + +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; + +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + +LET zero = (SELECT count(*) FROM svar_test); + +-- result should be 1000 +SELECT zero; + +DROP VARIABLE zero; +DROP TABLE svar_test; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; + +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; + +SELECT * FROM var1view; + +SET debug_parallel_query TO on; + +SELECT * FROM var1view; + +SET debug_parallel_query TO off; + +DROP VIEW var1view; +DROP VARIABLE var1; + +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); + +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + +DROP TABLE svar_test; +DROP VARIABLE varid; + + +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + +DROP VARIABLE var1; + +CREATE TABLE xxtab(avar int); + +INSERT INTO xxtab VALUES(333); + +CREATE TYPE xxtype AS (avar int); + +CREATE VARIABLE xxtab AS xxtype; + +INSERT INTO xxtab VALUES(10); + +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + +-- should be ok +SELECT avar FROM xxtab; + +CREATE VARIABLE public.avar AS int; + +-- should be ok, see the table +SELECT avar FROM xxtab; + +-- should be ok +SELECT public.avar FROM xxtab; + +DROP VARIABLE xxtab; + +SELECT xxtab.avar FROM xxtab; + +DROP VARIABLE public.avar; + +DROP TYPE xxtype; + +DROP TABLE xxtab; + +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; + +CREATE TABLE public.svar(a int, b int); + +INSERT INTO public.svar VALUES(10, 20); + +LET public.svar = (100, 200, 300); + +-- should be ok +-- show table +SELECT * FROM public.svar; +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; + +CREATE TYPE ab AS (a integer, b integer); + +CREATE VARIABLE v_ab AS ab; + +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); + +-- we should see table +SELECT v_ab.a FROM v_ab; + +CREATE SCHEMA v_ab; + +CREATE VARIABLE v_ab.a AS integer; + +-- we should see table +SELECT v_ab.a FROM v_ab; + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; + +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; + +SET search_path TO public; + +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); + +-- should be ok +SELECT xxx_am; + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; + +-- should be still ok +SELECT xxx_am; + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; + +-- enhanced references should be ok +SELECT public.xxx_am.b; +SELECT :"DBNAME".xxx_am.b; + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); + +-- we should see table +SELECT xxx_am.b FROM xxx_am; +SELECT x.b FROM xxx_am x; + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; + +CREATE SCHEMA :"DBNAME"; + +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; + +SET search_path TO :"DBNAME"; + +-- should be ambiguous +SELECT :"DBNAME".b; + +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; + +CREATE TABLE :"DBNAME"(b int); + +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + +DROP TABLE :"DBNAME"; + +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; + +RESET search_path; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9e4f3c1444d..68e3c3c7b63 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1498,6 +1498,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey @@ -2603,6 +2604,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData @@ -2793,6 +2795,9 @@ SupportRequestRows SupportRequestSelectivity SupportRequestSimplify SupportRequestWFuncMonotonic +SVariable +SVariableData +SVariableState Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] v20241119-2-0001-Enhancing-catalog-for-support-session-variables-and-.patch (129.9K, 24-v20241119-2-0001-Enhancing-catalog-for-support-session-variables-and-.patch) download | inline diff: From d5d4e1c9c3eae38f1c3de6113064c0e65179bbde Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 15:41:55 +0100 Subject: [PATCH 01/22] Enhancing catalog for support session variables and related support This patch introduces new system catalog table pg_variable. This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. Both commands are implemented by this patch. Possibility to change owner, schema or rename. Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. This patch enhancing pg_dump and psql to support session variables. The changes are related to system catalog. This patch is not short, but the code is simple. --- doc/src/sgml/catalogs.sgml | 120 +++++++++ doc/src/sgml/ddl.sgml | 31 +++ doc/src/sgml/func.sgml | 13 + doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/ref/allfiles.sgml | 3 + .../sgml/ref/alter_default_privileges.sgml | 26 +- doc/src/sgml/ref/alter_variable.sgml | 178 ++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 7 +- doc/src/sgml/ref/create_variable.sgml | 151 +++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++ doc/src/sgml/ref/grant.sgml | 6 + doc/src/sgml/ref/revoke.sgml | 7 + doc/src/sgml/reference.sgml | 3 + src/backend/catalog/Makefile | 1 + src/backend/catalog/aclchk.c | 78 ++++++ src/backend/catalog/dependency.c | 6 + src/backend/catalog/meson.build | 1 + src/backend/catalog/namespace.c | 133 +++++++++ src/backend/catalog/objectaddress.c | 121 ++++++++- src/backend/catalog/pg_shdepend.c | 2 + src/backend/catalog/pg_variable.c | 255 ++++++++++++++++++ src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/seclabel.c | 1 + src/backend/commands/tablecmds.c | 41 +++ src/backend/parser/gram.y | 105 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 16 ++ src/backend/utils/adt/acl.c | 7 + src/backend/utils/cache/lsyscache.c | 113 ++++++++ src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 + src/bin/pg_dump/pg_dump.c | 195 +++++++++++++- src/bin/pg_dump/pg_dump.h | 19 ++ src/bin/pg_dump/pg_dump_sort.c | 6 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 96 +++++++ src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 42 ++- src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/namespace.h | 4 + src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 3 + src/include/catalog/pg_variable.h | 81 ++++++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 2 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/acl.h | 1 + src/include/utils/lsyscache.h | 9 + src/test/regress/expected/dependency.out | 17 ++ src/test/regress/expected/oidjoins.out | 4 + src/test/regress/expected/psql.out | 50 ++++ .../regress/expected/session_variables.out | 58 ++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 + src/test/regress/sql/psql.sql | 21 ++ src/test/regress/sql/session_variables.sql | 61 +++++ src/tools/pgindent/typedefs.list | 4 + 65 files changed, 2365 insertions(+), 26 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h create mode 100644 src/test/regress/expected/session_variables.out create mode 100644 src/test/regress/sql/session_variables.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 59bb833f48d..bdd2ca0152f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9734,4 +9739,119 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 08155b156a5..2e3f0b95fb0 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5298,6 +5298,37 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. + </para> + + <para> + Inside a query or an expression, a session variable can be + <quote>shadowed</quote> by a column with the same name. Similarly, the + name of a function or procedure argument or a PL/pgSQL variable (see + <xref linkend="plpgsql-declarations"/>) can shadow a session variable + in the routine's body. Such collisions of identifiers can be resolved + by using qualified identifiers: Session variables can be qualified with + the schema name, columns can use table aliases, routine variables can use + block labels, and routine arguments can use the routine name. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 1a0b85bb4d7..61e5c606e21 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25736,6 +25736,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index f54f25c1c6b..d0cb53763aa 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1664,6 +1664,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 89aacec4fab..041c78d432f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -51,6 +51,10 @@ GRANT { { USAGE | CREATE } ON SCHEMAS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -83,6 +87,12 @@ REVOKE [ GRANT OPTION FOR ] ON SCHEMAS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -117,14 +127,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, and types (including domains) can be - altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), and session + variables can be altered. For this command, functions include aggregates + and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard + term for functions and procedures taken together. In earlier PostgreSQL + releases, only the word <literal>FUNCTIONS</literal> was allowed. It is not + possible to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..d87570e7d8b --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..a834c876bc9 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..ec165ab96fc --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,151 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..78ff10fcf5e 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..626c0231d0d 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -137,6 +137,13 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] | CURRENT_ROLE | CURRENT_USER | SESSION_USER + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 1589a75fd53..7a90fcfc457 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index aaf96a965e4..3620eb356c6 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,19 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach(cell, objnames) + { + RangeVar *varvar = (RangeVar *) lfirst(cell); + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +870,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1005,6 +1055,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_SCHEMA; errormsg = gettext_noop("invalid privilege type %s for schema"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1196,6 +1250,12 @@ SetDefaultACL(InternalDefaultACL *iacls) this_privileges = ACL_ALL_RIGHTS_SCHEMA; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; + default: elog(ERROR, "unrecognized object type: %d", (int) iacls->objtype); @@ -1439,6 +1499,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_NAMESPACE: iacls.objtype = OBJECT_SCHEMA; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1499,6 +1562,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2732,6 +2798,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2843,6 +2912,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("must be owner of type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("must be owner of view %s"); break; @@ -2991,6 +3063,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4258,6 +4332,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_NAMESPACE; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 0489cbabcb8..c9d686665de 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 2f3ded8a0e7..bfdb4da3301 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 30807f91904..86e2258657d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -986,6 +987,67 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + ListCell *l; + + visible = false; + foreach(l, activeSearchPath) + { + Oid namespaceId = lfirst_oid(l); + + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3351,66 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid namespaceId; + Oid varoid = InvalidOid; + ListCell *l; + + if (nspname) + { + namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach(l, activeSearchPath) + { + namespaceId = lfirst_oid(l); + + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} /* * DeconstructQualifiedName @@ -5085,3 +5207,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + + if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid))) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(VariableIsVisible(oid)); +} diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 7520a982151..2c8b9d7fd3c 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2005,16 +2027,20 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_NAMESPACE: objtype_str = "schemas"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, - DEFACLOBJ_NAMESPACE))); + DEFACLOBJ_NAMESPACE, + DEFACLOBJ_VARIABLE))); } /* @@ -2098,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2292,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2463,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3451,6 +3497,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -3803,6 +3875,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new schemas belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -4619,6 +4701,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -5725,6 +5811,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on schemas"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) @@ -5965,6 +6055,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, varname); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 753afb88453..1ea3d6db05c 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..e3a861c8cba --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,255 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +static ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + + +/* + * Creates entry in pg_variable table + */ +static ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + Acl *varacl; + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation;; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + /* we want SessionVariableCreatePostprocess to see the catalog changes. */ + CommandCounterIncrement(); + + return variable; +} + +/* + * Drop variable by OID, and register the needed session variable + * cleanup. + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index a45f3bb6b83..d4d88d11314 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -139,6 +140,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -418,6 +423,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -558,6 +564,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -640,6 +647,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -870,6 +878,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 85eec7e3947..3cce17e92af 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index dcfc1dbaffd..d03cb947e67 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 5607273bf9f..c026ab5dcb8 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index c632d1e8245..3deef69295d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6768,6 +6769,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6831,6 +6833,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 67eb96396af..89fa23c2dd4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -281,8 +282,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -775,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES + VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1042,6 +1043,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1584,6 +1586,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5210,6 +5213,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -6998,6 +7029,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -7874,6 +7906,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -7919,6 +7959,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8116,6 +8164,7 @@ defacl_privilege_target: | SEQUENCES { $$ = OBJECT_SEQUENCE; } | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -9904,6 +9953,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10265,6 +10332,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10546,6 +10631,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -17917,6 +18010,8 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18570,6 +18665,8 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 0f324ee4e31..4eaed1f8439 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4039,6 +4040,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4107,6 +4109,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4120,6 +4131,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f28bf371059..217c6028497 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -23,6 +23,7 @@ #include "catalog/namespace.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" +#include "catalog/pg_variable.h" #include "catalog/toasting.h" #include "commands/alter.h" #include "commands/async.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 2a716cc6b7f..98522a45612 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -851,6 +851,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +952,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index a85dc0d891f..b464a7f090a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -38,6 +38,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3714,3 +3715,115 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} + +/* + * Returns the type of the given session variable. + */ +Oid +get_session_variable_type(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid vartype; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + vartype = varform->vartype; + + ReleaseSysCache(tup); + + return vartype; +} + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 33d323085f5..b0451c1b903 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 5649859aa1e..50de5d0ea29 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -511,6 +511,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index 68ae2970ade..3fa61090c64 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 8c20c263c4b..6f012a1b558 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3098,6 +3098,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3647,6 +3655,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index a8c141b689d..b8d839ece41 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -363,6 +363,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5416,6 +5417,190 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + resetPQExpBuffer(query); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || dopt->dataOnly) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -10140,7 +10325,8 @@ getAdditionalACLs(Archive *fout) dobj->objType == DO_TABLE || dobj->objType == DO_PROCLANG || dobj->objType == DO_FDW || - dobj->objType == DO_FOREIGN_SERVER) + dobj->objType == DO_FOREIGN_SERVER || + dobj->objType == DO_VARIABLE) { DumpableObjectWithAcl *daobj = (DumpableObjectWithAcl *) dobj; @@ -10739,6 +10925,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_SUBSCRIPTION_REL: dumpSubscriptionTable(fout, (const SubRelInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -15188,6 +15377,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_NAMESPACE: type = "SCHEMAS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -18898,6 +19090,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index d65f5585651..f2f558b2961 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -52,6 +52,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -699,6 +700,23 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + char *varacl; + char *rvaracl; + char *initvaracl; + char *initrvaracl; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -782,5 +800,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 3c8f2eb808d..ca323171aa8 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1500,6 +1502,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "POST-DATA BOUNDARY (ID %d)", obj->dumpId); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index aa1564cd450..dd8c054a6a6 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -789,6 +789,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1674,6 +1684,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -3942,6 +3969,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4404,6 +4449,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 1f3cbb11f7c..7594f1010a6 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1077,6 +1077,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 5bfebad64d5..c52c8fd147f 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5195,6 +5195,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 273f974538e..4e15a180ea6 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 02fe5d151e0..317bc2eab6e 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -265,6 +265,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[S+] [PATTERN] list data types\n"); HELP0(" \\du[S+] [PATTERN] list roles\n"); HELP0(" \\dv[S+] [PATTERN] list views\n"); + HELP0(" \\dV [PATTERN] list session variables\n"); HELP0(" \\dx[+] [PATTERN] list extensions\n"); HELP0(" \\dX [PATTERN] list extended statistics\n"); HELP0(" \\dy[+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fad2277991d..293648e30c7 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1304,6 +1311,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1865,7 +1873,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\errverbose", "\\ev", "\\f", @@ -2556,6 +2564,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3121,7 +3132,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3931,6 +3942,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4193,6 +4211,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4394,7 +4418,7 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4416,7 +4440,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4424,7 +4449,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4460,6 +4486,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4762,7 +4790,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5228,6 +5256,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 167f91a6e3f..507f3808218 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index f70d1daba52..6eec79d2eec 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8d434d48d57..18ea6ac83a5 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,8 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index d272cdf08b4..529d53c6bb6 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -68,6 +68,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_FUNCTION 'f' /* function */ #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index cbbe8acd382..41f1121d19f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6566,6 +6566,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9221', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..3810e040f28 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +extern ObjectAddress CreateVariable(ParseState *pstate, + CreateSessionVarStmt *stmt); +extern void DropVariableById(Oid varid); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +#endif /* PG_VARIABLE_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0f9462493e3..0e08296b438 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2316,6 +2316,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3447,6 +3448,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 899d64ad55f..56670e65b11 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -484,6 +484,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 7fdcec6dd93..9465df7b2ff 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 731d84b2a93..98aab98229e 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 20446f6f836..8a3684e711f 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -134,6 +134,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -206,6 +207,14 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); +extern Oid get_session_variable_type(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 74d9ff2998d..c336f67a596 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 36dc31c16c4..88e21194711 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5930,6 +5930,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6151,6 +6175,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6385,6 +6415,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6554,6 +6590,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of relations @@ -6687,6 +6729,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6734,6 +6782,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out new file mode 100644 index 00000000000..d0aff56a01d --- /dev/null +++ b/src/test/regress/expected/session_variables.out @@ -0,0 +1,58 @@ +CREATE ROLE regress_variable_owner; +-- should be ok +CREATE VARIABLE var1 AS int; +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; +ERROR: session variable cannot be pseudo-type anyelement +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; +NOTICE: session variable "var2" does not exist, skipping +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; +NOTICE: session variable "var1" already exists, skipping +-- should fail +CREATE VARIABLE var1 AS int; +ERROR: session variable "var1" already exists +-- should be ok +DROP VARIABLE IF EXISTS var1; +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + obj_description +----------------------- + some variable comment +(1 row) + +DROP VARIABLE var1; +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +CREATE ROLE regress_variable_reader; +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; +DROP VARIABLE public.varxx; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; +\dV+ svartest.var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| + | | | | | regress_variable_reader=r/regress_variable_owner | +(1 row) + +DROP VARIABLE svartest.var1; +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 81e4222d26a..05eb784f62b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -111,7 +111,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does a reconnect which transiently uses 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index c5021fc0b13..666eddb632d 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1631,6 +1631,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1742,6 +1755,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1783,6 +1799,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1823,6 +1841,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1847,6 +1866,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1872,6 +1892,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql new file mode 100644 index 00000000000..82679cbf48e --- /dev/null +++ b/src/test/regress/sql/session_variables.sql @@ -0,0 +1,61 @@ +CREATE ROLE regress_variable_owner; + +-- should be ok +CREATE VARIABLE var1 AS int; + +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; + +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; + +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; + +-- should fail +CREATE VARIABLE var1 AS int; + +-- should be ok +DROP VARIABLE IF EXISTS var1; + +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + +DROP VARIABLE var1; + +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; + +CREATE VARIABLE svartest.var1 AS int; + +CREATE ROLE regress_variable_reader; + +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; + +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; + +DROP VARIABLE public.varxx; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; + +\dV+ svartest.var1 + +DROP VARIABLE svartest.var1; + +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 08521d51a9b..9e4f3c1444d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -535,6 +535,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext @@ -874,6 +875,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -933,6 +935,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData @@ -3079,6 +3082,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.47.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2024-11-20 07:25 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Pavel Stehule @ 2024-11-20 07:25 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]> Hi fix missing support for VariableFence in raw_expression_tree_walker_impl Regards Pavel Attachments: [text/x-patch] v20241120-0019-transactional-variables.patch (39.2K, 3-v20241120-0019-transactional-variables.patch) download | inline diff: From dab872f1ebc0b2248690799e7667a5de8ce62909 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:11:33 +0100 Subject: [PATCH 19/21] transactional variables This commit implements transactional variables. The content of transactional session variables is sensitive to transactions and subtransactions. Any transactional variable holds a history of values necessary for revert. This history is cleaned (purged) when a) we read the variable, b) when the transaction is finished. We don't try to purge, when the last modification of a variable is from current subtransaction, or when purge was processed in current subtransaction. This patch is based on my work from Feb 2020 and it is related to discussion about features related to session variables. Now, when the all features are separated to isolated patches I can revitalize this patch, because it doesn't increase complexity of basic patches. Unlike other patches, this patch is without any review at this moment (Feb 2024), but the feature should be fully functional for people who are interested about this feature (for testing). --- doc/src/sgml/catalogs.sgml | 12 + doc/src/sgml/ref/create_variable.sgml | 10 +- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 301 +++++++++++++++++- src/backend/parser/gram.y | 50 +-- src/bin/pg_dump/pg_dump.c | 10 +- src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 +++ src/bin/psql/describe.c | 4 +- src/bin/psql/tab-complete.in.c | 5 + src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/kwlist.h | 1 + src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 110 ++++++- src/test/regress/sql/session_variables.sql | 58 ++++ 16 files changed, 592 insertions(+), 50 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 9a8886fc69a..1064b3749e3 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9859,6 +9859,18 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry><structfield>varistransact</structfield></entry> + <entry><type>boolean</type></entry> + <entry></entry> + <entry> + True, when the variable is <quote>transactional</quote>. In the case + of a transaction rollback, transactional variables are reset to the + value they had when the transaction started. The default value is + <literal>false</literal>. + </entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>vareoxaction</structfield> <type>char</type> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 00938743c0d..b73f032ddbb 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] [ { TRANSACTIONAL | TRANSACTION } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> @@ -47,6 +47,14 @@ CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replac same as regular variables in procedural languages. </para> + <para> + When a schema variable is created with a + <command>CREATE TRANSACTIONAL VARIABLE</command> command, the variables + content changes are transactional: in case of rollback, they are reset to + their value at the beginning of the transaction or the latest subtransaction. + The variable content is only hold in memory, and thus is not persistent. + </para> + <para> Session variables are retrieved by the <command>SELECT</command> command. Their value is set with the <command>LET</command> command. diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index f261e3fd770..771c2816048 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -42,6 +42,7 @@ static ObjectAddress create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -59,6 +60,7 @@ create_variable(const char *varName, bool if_not_exists, bool not_null, bool is_immutable, + bool is_transact, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -123,6 +125,7 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); + values[Anum_pg_variable_varistransact - 1] = BoolGetDatum(is_transact); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -267,6 +270,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) stmt->if_not_exists, stmt->not_null, stmt->is_immutable, + stmt->is_transact, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 7f34f93b542..978c84c9bdb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -47,6 +47,19 @@ typedef struct SVariableXActDropItem SubTransactionId deleting_subid; } SVariableXActDropItem; +/* + * Used for transactional variables. Holds prev version. + */ +typedef struct PrevValue +{ + Datum value; + bool isnull; + + SubTransactionId modify_subid; + + struct PrevValue *prev_value; +} PrevValue; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -68,6 +81,25 @@ typedef struct SVariableData bool isnull; Datum value; + /* + * We don't need stack versions modified in same subtransaction. + * Used by transactional variables only. The value of transactional + * variable can be returned immediately when modify_subid is same + * like current subid. + */ + SubTransactionId modify_subid; + + /* + * When the modify_subid is different than current subid, then + * we need to recheck versions and throw versions related to + * reverted transactions. When purge_subid is same like current subid + * we can return the value of transaction variable without this + * recheck. + */ + SubTransactionId purge_subid; + + PrevValue *prev_value; + Oid typid; int16 typlen; bool typbyval; @@ -86,6 +118,7 @@ typedef struct SVariableData bool not_null; bool is_immutable; + bool is_transact; bool reset_at_eox; @@ -125,6 +158,9 @@ static bool needs_validation = false; */ static bool has_session_variables_with_reset_at_eox = false; +/* true, when transactional variables was modified */ +static bool has_modified_transactional_variables = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -140,6 +176,7 @@ static List *xact_drop_items = NIL; static void register_session_variable_xact_drop(Oid varid); static void unregister_session_variable_xact_drop(Oid varid); +static bool purge_session_variable(SVariable svar); /* * Callback function for session variable invalidation. @@ -292,8 +329,25 @@ unregister_session_variable_xact_drop(Oid varid) * Release stored value, free memory */ static void -free_session_variable_value(SVariable svar) +free_session_variable_value(SVariable svar, bool deep_free) { + if (deep_free) + { + PrevValue *prev_value = svar->prev_value; + PrevValue *next_value; + + while (prev_value) + { + if (!svar->typbyval) + pfree(DatumGetPointer(prev_value->value)); + + next_value = prev_value->prev_value; + pfree(prev_value); + + prev_value = next_value; + } + } + /* clean the current value */ if (!svar->isnull) { @@ -390,7 +444,7 @@ remove_invalid_session_variables(bool atEOX) { Oid varid = svar->varid; - free_session_variable_value(svar); + free_session_variable_value(svar, true); hash_search(sessionvars, &varid, HASH_REMOVE, NULL); svar = NULL; } @@ -426,6 +480,94 @@ remove_session_variables_with_reset_at_eox(void) has_session_variables_with_reset_at_eox = false; } +/* + * remove prev values at eox + */ +static void +remove_prev_values_at_eox(bool isCommit) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_modified_transactional_variables) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->is_transact && svar->modify_subid != InvalidSubTransactionId) + { + if (isCommit) + { + if (purge_session_variable(svar)) + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + else + { + PrevValue *prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId) + break; + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + if (prev_value) + { + svar->value = prev_value->value; + svar->isnull = prev_value->isnull; + + pfree(prev_value); + } + else + { + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + svar = NULL; + } + } + + /* when svar is still valid (not removed from sessionvars */ + if (svar) + { + svar->modify_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + } + } + } + + has_modified_transactional_variables = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -473,6 +615,8 @@ AtPreEOXact_SessionVariables(bool isCommit) remove_invalid_session_variables(true); } + remove_prev_values_at_eox(isCommit); + /* * We have to clean xact_drop_items. All related variables are dropped * now, or lost inside aborted transaction. @@ -618,6 +762,7 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->not_null = varform->varnotnull; svar->is_immutable = varform->varisimmutable; + svar->is_transact = varform->varistransact; svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; @@ -643,6 +788,17 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->isnull = true; svar->value = (Datum) 0; + if (svar->is_transact) + { + svar->modify_subid = GetCurrentSubTransactionId(); + has_modified_transactional_variables = true; + } + else + svar->modify_subid = InvalidSubTransactionId; + + svar->purge_subid = InvalidSubTransactionId; + svar->prev_value = NULL; + svar->is_valid = true; svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, @@ -676,6 +832,69 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) ReleaseSysCache(tup); } +/* + * Try to remove all previous versions related to reverted transactions. + * Returns true, when valid version was found. + */ +static bool +purge_session_variable(SVariable svar) +{ + SubTransactionId current_subid; + PrevValue *prev_value; + bool found = true; + + Assert(svar->is_transact); + + if (svar->modify_subid == InvalidSubTransactionId) + return true; + + current_subid = GetCurrentSubTransactionId(); + + if (svar->modify_subid == current_subid) + return true; + + if (svar->purge_subid == current_subid) + return true; + + if (SubTransactionIsActive(svar->modify_subid)) + { + svar->purge_subid = current_subid; + return true; + } + + prev_value = svar->prev_value; + + while (prev_value) + { + PrevValue *current_pv = prev_value; + + if (current_pv->modify_subid == InvalidSubTransactionId || + SubTransactionIsActive(current_pv->modify_subid)) + { + svar->value = current_pv->value; + svar->isnull = current_pv->isnull; + svar->modify_subid = current_pv->modify_subid; + + prev_value = current_pv->prev_value; + pfree(current_pv); + + found = true; + break; + } + + if (!svar->typbyval && !current_pv->isnull) + pfree(DatumGetPointer(current_pv->value)); + + prev_value = current_pv->prev_value; + pfree(current_pv); + } + + svar->prev_value = prev_value; + svar->purge_subid = current_subid; + + return found; +} + /* * Assign a new value to the session variable. It is copied to * SVariableMemoryContext if necessary. @@ -688,6 +907,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Datum newval; SVariableData locsvar, *_svar; + SubTransactionId current_subid = GetCurrentSubTransactionId(); + SubTransactionId prev_purge_subid = InvalidSubTransactionId; + bool save_prev_value; + PrevValue *prev_value; Assert(svar); Assert(!isnull || value == (Datum) 0); @@ -723,6 +946,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else _svar = svar; + if (_svar->is_transact && _svar->create_lsn == svar->create_lsn) + { + Assert(svar->typid == _svar->typid); + Assert(svar->typbyval == _svar->typbyval); + Assert(svar->typlen == _svar->typlen); + + save_prev_value = svar->modify_subid != current_subid; + prev_value = svar->prev_value; + } + else + { + save_prev_value = false; + prev_value = NULL; + } + if (!isnull) { MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); @@ -734,7 +972,37 @@ set_session_variable(SVariable svar, Datum value, bool isnull) else newval = value; - free_session_variable_value(svar); + if (save_prev_value) + { + volatile PrevValue *new_prev_value; + + PG_TRY(); + { + new_prev_value = MemoryContextAlloc(SVariableMemoryContext, + sizeof(PrevValue)); + } + PG_CATCH(); + { + /* release mem from persistent content */ + if (newval != value) + pfree(DatumGetPointer(newval)); + PG_RE_THROW(); + } + PG_END_TRY(); + + new_prev_value->value = svar->value; + new_prev_value->isnull = svar->isnull; + new_prev_value->modify_subid = svar->modify_subid; + new_prev_value->prev_value = prev_value; + + prev_value = (PrevValue *) new_prev_value; + prev_purge_subid = svar->purge_subid; + + has_modified_transactional_variables = true; + + } + else + free_session_variable_value(svar, prev_value == NULL); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", get_namespace_name(get_session_variable_namespace(svar->varid)), @@ -747,6 +1015,9 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + svar->modify_subid = current_subid; + svar->purge_subid = prev_purge_subid; + svar->prev_value = prev_value; /* don't allow more changes of value when variable is IMMUTABLE */ if (svar->is_immutable) @@ -848,6 +1119,8 @@ get_session_variable(Oid varid) else svar->is_valid = false; +reinit: + /* * Force setup for not yet initialized variables or variables that cannot * be validated. @@ -880,6 +1153,28 @@ get_session_variable(Oid varid) varid); } + /* + * Transactional variables should be purged before (remove + * versions created by possibly reverted subtransactions). + */ + if (svar->is_transact && + svar->modify_subid != GetCurrentSubTransactionId() && + svar->modify_subid != InvalidSubTransactionId) + { + if (!purge_session_variable(svar)) + { + /* force reinit */ + svar->is_valid = false; + + /* + * In next iteration modify_subid should be + * InvalidSubTransactionId or current subid, + * so there is not risk of infinity cycle. + */ + goto reinit; + } + } + /* ensure the returned data is still of the correct domain */ if (svar->is_domain) { diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index bc8e25b1933..af9d640c0e2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,7 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt -%type <boolean> OptNotNull OptImmutable +%type <boolean> OptNotNull OptImmutable OptTransactional /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -773,7 +773,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER TABLE TABLES TABLESAMPLE TABLESPACE TARGET TEMP TEMPLATE TEMPORARY TEXT_P THEN - TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM + TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSACTIONAL TRANSFORM TREAT TRIGGER TRIM TRUE_P TRUNCATE TRUSTED TYPE_P TYPES_P @@ -5240,31 +5240,33 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptTransactional OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $5->relpersistence = $2; - n->is_immutable = $3; - n->variable = $5; - n->typeName = $7; - n->collClause = (CollateClause *) $8; - n->not_null = $9; - n->defexpr = $10; - n->XactEndAction = $11; + $6->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->not_null = $10; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptTransactional OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $8->relpersistence = $2; - n->is_immutable = $3; - n->variable = $8; - n->typeName = $10; - n->collClause = (CollateClause *) $11; - n->not_null = $12; - n->defexpr = $13; - n->XactEndAction = $14; + $9->relpersistence = $2; + n->is_immutable = $4; + n->is_transact = $3; + n->variable = $9; + n->typeName = $11; + n->collClause = (CollateClause *) $12; + n->not_null = $13; + n->defexpr = $14; + n->XactEndAction = $15; n->if_not_exists = true; $$ = (Node *) n; } @@ -5292,6 +5294,12 @@ OptImmutable: IMMUTABLE { $$ = true; } | /* EMPTY */ { $$ = false; } ; +OptTransactional: + TRANSACTION { $$ = true; } + | TRANSACTIONAL { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -18084,6 +18092,7 @@ unreserved_keyword: | TEXT_P | TIES | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TRIGGER | TRUNCATE @@ -18732,6 +18741,7 @@ bare_label_keyword: | TIMESTAMP | TRAILING | TRANSACTION + | TRANSACTIONAL | TRANSFORM | TREAT | TRIGGER diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 59f4103ca4f..762cd020ea9 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5439,6 +5439,7 @@ getVariables(Archive *fout) int i_varcollation; int i_varnotnull; int i_varisimmutable; + int i_varistransact; int i_varacl; int i_acldefault; int i, @@ -5463,6 +5464,7 @@ getVariables(Archive *fout) " END AS varcollation,\n" " v.varnotnull,\n" " v.varisimmutable,\n" + " v.varistransact,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5485,6 +5487,7 @@ getVariables(Archive *fout) i_varcollation = PQfnumber(res, "varcollation"); i_varnotnull = PQfnumber(res, "varnotnull"); i_varisimmutable = PQfnumber(res, "varisimmutable"); + i_varistransact = PQfnumber(res, "varistransact"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5513,6 +5516,7 @@ getVariables(Archive *fout) varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; + varinfo[i].varistransact = *(PQgetvalue(res, i, i_varistransact)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5560,6 +5564,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vardefexpr; const char *varxactendaction; const char *varisimmutable; + const char *varistransact; Oid varcollation; bool varnotnull; @@ -5577,12 +5582,13 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) varcollation = varinfo->varcollation; varnotnull = varinfo->varnotnull; varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; + varistransact = varinfo->varistransact ? "TRANSACTIONAL " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", - varisimmutable, qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %s%sVARIABLE %s AS %s", + varistransact, varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f7921f942d0..310701f09e1 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -719,6 +719,7 @@ typedef struct _VariableInfo const char *rolname; /* name of owner, or empty string */ bool varnotnull; bool varisimmutable; + bool varistransact; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 47cb59356e0..b39bddd3e40 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4077,6 +4077,42 @@ my %tests = ( }, }, + 'CREATE TRANSACTIONAL VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL VARIABLE dump_test.variable8 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer', + regexp => qr/^ + \QCREATE TRANSACTIONAL IMMUTABLE VARIABLE dump_test.variable9 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index cc69f07f1f9..8862539d76e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " NOT v.varnotnull as \"%s\",\n" " NOT v.varisimmutable as \"%s\",\n" + " v.varistransact as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5241,6 +5242,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Owner"), gettext_noop("Nullable"), gettext_noop("Mutable"), + gettext_noop("Transactional"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b653dda1fe8..e955522d2cb 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1305,6 +1305,8 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"TEXT SEARCH", NULL, NULL, NULL}, {"TRANSFORM", NULL, NULL, NULL, NULL, THING_NO_ALTER}, + {"TRANSACTIONAL", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE TRANSACTIONAL + * VARIABLE ... */ {"TRIGGER", "SELECT tgname FROM pg_catalog.pg_trigger WHERE tgname LIKE '%s' AND NOT tgisinternal"}, {"TYPE", NULL, NULL, &Query_for_list_of_datatypes}, {"UNIQUE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE UNIQUE @@ -3947,8 +3949,11 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ + else if (Matches("CREATE", "TRANSACTION|TRANSACTIONAL")) + COMPLETE_WITH("IMMUTABLE", "VARIABLE"); else if (TailMatches("CREATE", "VARIABLE", MatchAny) || TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("TRANSACTION|TRANSACTIONAL", "VARIABLE", MatchAny) || TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 68bc49a0e27..1dc44edf6e5 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -61,6 +61,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* don't allow changes */ bool varisimmutable BKI_DEFAULT(f); + /* supports transactions */ + bool varistransact BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bf820828dd9..a4372f184df 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3481,6 +3481,7 @@ typedef struct CreateSessionVarStmt bool if_not_exists; /* do nothing if it already exists */ bool not_null; /* disallow nulls */ bool is_immutable; /* don't allow changes */ + bool is_transact; /* supports transactions */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index aefe51f335b..125eb819b76 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -455,6 +455,7 @@ PG_KEYWORD("timestamp", TIMESTAMP, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("to", TO, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("trailing", TRAILING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transaction", TRANSACTION, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("transactional", TRANSACTIONAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("transform", TRANSFORM, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("treat", TREAT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("trigger", TRIGGER, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index e1635f686c2..da9f4a7030a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | t | t | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action ---------+------+------+-----------+-------+----------+---------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 5a66fc9ffab..3e6b73684fd 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Transactional | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | f | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1763,3 +1763,103 @@ SELECT var1; LET var1 = 30; ERROR: session variable "public.var1" is declared IMMUTABLE DROP VARIABLE var1; +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; +BEGIN; + LET tv = 100; + SELECT tv; + tv +----- + 100 +(1 row) + +ROLLBACK; +SELECT tv; + tv +---- + 0 +(1 row) + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 0; + SELECT tv; + tv +---- + 0 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +-- test subtransactions +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +ROLLBACK; +SELECT tv; + tv +------ + 1000 +(1 row) + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + tv +---- + 2 +(1 row) + + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; + tv +---- + 1 +(1 row) + +COMMIT; +SELECT tv; + tv +---- + 1 +(1 row) + +DROP VARIABLE tv; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index fcd783e966c..68fc0cde44a 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1208,3 +1208,61 @@ SELECT var1; LET var1 = 30; DROP VARIABLE var1; + +-- test transactional variables +CREATE TRANSACTION VARIABLE tv AS int DEFAULT 0; + +BEGIN; + LET tv = 100; + SELECT tv; +ROLLBACK; + +SELECT tv; + +LET tv = 100; +BEGIN; + LET tv = 1000; +COMMIT; + +SELECT tv; + +BEGIN; + LET tv = 0; + SELECT tv; +ROLLBACK; + +SELECT tv; + +-- test subtransactions + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +ROLLBACK; + +SELECT tv; + +BEGIN; + LET tv = 1; +SAVEPOINT x1; + LET tv = 2; +SAVEPOINT x2; + LET tv = 3; +ROLLBACK TO x2; + SELECT tv; + LET tv = 10; +ROLLBACK TO x1; + SELECT tv; +COMMIT; + +SELECT tv; + +DROP VARIABLE tv; -- 2.47.0 [text/x-patch] v20241120-0018-this-patch-changes-error-message-column-doesn-t-exis.patch (29.2K, 4-v20241120-0018-this-patch-changes-error-message-column-doesn-t-exis.patch) download | inline diff: From 39c1a127cf6b8a26b963c3808271fee0eb2a769b Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:10:39 +0100 Subject: [PATCH 18/21] this patch changes error message "column doesn't exist" to message "column or variable doesn't exist" The error message will be more correct. Today, missing PL/pgSQL variable can be reported. The change has impact on lot of regress tests not directly related to session variables, and then it is distributed as separate patch --- src/backend/parser/parse_expr.c | 2 +- src/backend/parser/parse_relation.c | 24 +++++++++++--- src/backend/parser/parse_target.c | 8 +++-- src/include/parser/parse_expr.h | 2 ++ src/pl/plpgsql/src/expected/plpgsql_array.out | 2 +- .../plpgsql/src/expected/plpgsql_record.out | 4 +-- .../src/expected/plpgsql_session_variable.out | 2 +- src/pl/tcl/expected/pltcl_queries.out | 12 +++---- .../isolation/expected/session-variable.out | 4 +-- src/test/regress/expected/alter_table.out | 32 +++++++++---------- src/test/regress/expected/copy2.out | 2 +- src/test/regress/expected/errors.out | 8 ++--- src/test/regress/expected/join.out | 12 +++---- src/test/regress/expected/namespace.out | 2 +- src/test/regress/expected/numerology.out | 2 +- src/test/regress/expected/plpgsql.out | 12 +++---- src/test/regress/expected/psql.out | 2 +- src/test/regress/expected/rules.out | 2 +- .../regress/expected/session_variables.out | 6 ++-- src/test/regress/expected/transactions.out | 4 +-- src/test/regress/expected/union.out | 2 +- 21 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index bcbb1f564af..16bf52c9854 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -520,7 +520,7 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) * printed. When we are in an expression where session variables cannot be * used, we raise the first form of error message. */ -static bool +bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind) { bool result = false; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 95ae2108044..fa9a30bd138 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -27,6 +27,7 @@ #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/parse_enr.h" +#include "parser/parse_expr.h" #include "parser/parse_relation.h" #include "parser/parse_type.h" #include "parser/parsetree.h" @@ -3730,6 +3731,19 @@ errorMissingRTE(ParseState *pstate, RangeVar *relation) parser_errposition(pstate, relation->location))); } +/* + * set message "column does not exist" or "column or variable does not exist" + * in dependency if expression context allows session variables. + */ +static int +column_or_variable_does_not_exists(ParseState *pstate, const char *colname) +{ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + return errmsg("column or variable \"%s\" does not exist", colname); + else + return errmsg("column \"%s\" does not exist", colname); +} + /* * Generate a suitable error about a missing column. * @@ -3764,7 +3778,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There are columns named \"%s\", but they are in tables that cannot be referenced from this part of the query.", colname), !relname ? errhint("Try using a table-qualified name.") : 0, @@ -3774,7 +3788,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errdetail("There is a column named \"%s\" in table \"%s\", but it cannot be referenced from this part of the query.", colname, state->rexact1->eref->aliasname), rte_visible_if_lateral(pstate, state->rexact1) ? @@ -3792,14 +3806,14 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), parser_errposition(pstate, location))); /* Handle case where we have a single alternative spelling to offer */ ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, @@ -3813,7 +3827,7 @@ errorMissingColumn(ParseState *pstate, (errcode(ERRCODE_UNDEFINED_COLUMN), relname ? errmsg("column %s.%s does not exist", relname, colname) : - errmsg("column \"%s\" does not exist", colname), + column_or_variable_does_not_exists(pstate, colname), errhint("Perhaps you meant to reference the column \"%s.%s\" or the column \"%s.%s\".", state->rfirst->eref->aliasname, strVal(list_nth(state->rfirst->eref->colnames, diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 76bf88c3ca2..6af6b801ad4 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -779,7 +779,9 @@ transformAssignmentIndirection(ParseState *pstate, if (!typrelid) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because its type %s is not a composite type" : + "cannot assign to field \"%s\" of column \"%s\" because its type %s is not a composite type", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); @@ -788,7 +790,9 @@ transformAssignmentIndirection(ParseState *pstate, if (attnum == InvalidAttrNumber) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", + errmsg(expr_kind_allows_session_variables(pstate->p_expr_kind) ? + "cannot assign to field \"%s\" of column or variable \"%s\" because there is no such column in data type %s" : + "cannot assign to field \"%s\" of column \"%s\" because there is no such column in data type %s", strVal(n), targetName, format_type_be(targetTypeId)), parser_errposition(pstate, location))); diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 0d64b300f8a..e5aebf99b4a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -23,4 +23,6 @@ extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKin extern const char *ParseExprKindName(ParseExprKind exprKind); +extern bool expr_kind_allows_session_variables(ParseExprKind p_expr_kind); + #endif /* PARSE_EXPR_H */ diff --git a/src/pl/plpgsql/src/expected/plpgsql_array.out b/src/pl/plpgsql/src/expected/plpgsql_array.out index ad60e0e8be3..c1ab9fd7ed9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_array.out +++ b/src/pl/plpgsql/src/expected/plpgsql_array.out @@ -41,7 +41,7 @@ NOTICE: a = {"(,11)"}, a[1].i = 11 -- perhaps this ought to work, but for now it doesn't: do $$ declare a complex[]; begin a[1:2].i := array[11,12]; raise notice 'a = %', a; end$$; -ERROR: cannot assign to field "i" of column "a" because its type complex[] is not a composite type +ERROR: cannot assign to field "i" of column or variable "a" because its type complex[] is not a composite type LINE 1: a[1:2].i := array[11,12] ^ QUERY: a[1:2].i := array[11,12] diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index 6974c8f4a44..3970ac721c9 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -135,7 +135,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ declare c nested_int8s; begin c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "c" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "c" because there is no such column in data type two_int8s LINE 1: c.c2.x = 1 ^ QUERY: c.c2.x = 1 @@ -157,7 +157,7 @@ ERROR: record "c" has no field "x" CONTEXT: PL/pgSQL assignment "b.c.x.q1 = 1" PL/pgSQL function inline_code_block line 1 at assignment do $$ <<b>> declare c nested_int8s; begin b.c.c2.x = 1; end $$; -ERROR: cannot assign to field "x" of column "b" because there is no such column in data type two_int8s +ERROR: cannot assign to field "x" of column or variable "b" because there is no such column in data type two_int8s LINE 1: b.c.c2.x = 1 ^ QUERY: b.c.c2.x = 1 diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ecac64fcfb4..8bd2b398bdd 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -232,7 +232,7 @@ SET ROLE TO DEFAULT; SET ROLE TO regress_var_exec_role; -- should fail, but not crash SELECT var_read_func(); -ERROR: column "plpgsql_sv_var1" does not exist +ERROR: column or variable "plpgsql_sv_var1" does not exist LINE 1: plpgsql_sv_var1 ^ QUERY: plpgsql_sv_var1 diff --git a/src/pl/tcl/expected/pltcl_queries.out b/src/pl/tcl/expected/pltcl_queries.out index 35cc6e62aad..497cd38189b 100644 --- a/src/pl/tcl/expected/pltcl_queries.out +++ b/src/pl/tcl/expected/pltcl_queries.out @@ -450,12 +450,12 @@ CONTEXT: while executing "__PLTcl_proc_tcl_eval_text spi_prepare\ a\ \"b\ \{\"" in PL/Tcl function tcl_eval(text) select tcl_error_handling_test($tcl$spi_prepare "select moo" []$tcl$); - tcl_error_handling_test --------------------------------------- - SQLSTATE: 42703 + - condition: undefined_column + - cursor_position: 8 + - message: column "moo" does not exist+ + tcl_error_handling_test +-------------------------------------------------- + SQLSTATE: 42703 + + condition: undefined_column + + cursor_position: 8 + + message: column or variable "moo" does not exist+ statement: select moo (1 row) diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index e9a254bcd12..d8848d555cd 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -10,7 +10,7 @@ test step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist starting permutation: let val s1 drop val sr1 step let: LET myvar = 'test'; @@ -23,7 +23,7 @@ test step s1: BEGIN; step drop: DROP VARIABLE myvar; step val: SELECT myvar; -ERROR: column "myvar" does not exist +ERROR: column or variable "myvar" does not exist step sr1: ROLLBACK; starting permutation: let val dbg drop create dbg val diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 2212c8dbb59..35b3a3bd302 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -1295,19 +1295,19 @@ select * from atacc1; (1 row) select * from atacc1 order by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 order by a; ^ select * from atacc1 order by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 order by "........pg.dropped.1........"... ^ select * from atacc1 group by a; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 group by a; ^ select * from atacc1 group by "........pg.dropped.1........"; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 group by "........pg.dropped.1........"... ^ select atacc1.* from atacc1; @@ -1317,7 +1317,7 @@ select atacc1.* from atacc1; (1 row) select a from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a from atacc1; ^ select atacc1.a from atacc1; @@ -1331,15 +1331,15 @@ select b,c,d from atacc1; (1 row) select a,b,c,d from atacc1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select a,b,c,d from atacc1; ^ select * from atacc1 where a = 1; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: select * from atacc1 where a = 1; ^ select "........pg.dropped.1........" from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........" from atacc1; ^ select atacc1."........pg.dropped.1........" from atacc1; @@ -1347,11 +1347,11 @@ ERROR: column atacc1.........pg.dropped.1........ does not exist LINE 1: select atacc1."........pg.dropped.1........" from atacc1; ^ select "........pg.dropped.1........",b,c,d from atacc1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select "........pg.dropped.1........",b,c,d from atacc1; ^ select * from atacc1 where "........pg.dropped.1........" = 1; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: select * from atacc1 where "........pg.dropped.1........" = ... ^ -- UPDATEs @@ -1360,7 +1360,7 @@ ERROR: column "a" of relation "atacc1" does not exist LINE 1: update atacc1 set a = 3; ^ update atacc1 set b = 2 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: update atacc1 set b = 2 where a = 3; ^ update atacc1 set "........pg.dropped.1........" = 3; @@ -1368,7 +1368,7 @@ ERROR: column "........pg.dropped.1........" of relation "atacc1" does not exis LINE 1: update atacc1 set "........pg.dropped.1........" = 3; ^ update atacc1 set b = 2 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: update atacc1 set b = 2 where "........pg.dropped.1........"... ^ -- INSERTs @@ -1416,11 +1416,11 @@ LINE 1: insert into atacc1 ("........pg.dropped.1........",b,c,d) va... ^ -- DELETEs delete from atacc1 where a = 3; -ERROR: column "a" does not exist +ERROR: column or variable "a" does not exist LINE 1: delete from atacc1 where a = 3; ^ delete from atacc1 where "........pg.dropped.1........" = 3; -ERROR: column "........pg.dropped.1........" does not exist +ERROR: column or variable "........pg.dropped.1........" does not exist LINE 1: delete from atacc1 where "........pg.dropped.1........" = 3; ^ delete from atacc1; @@ -1706,7 +1706,7 @@ select f1 from c1; alter table c1 drop column f1; select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". @@ -1720,7 +1720,7 @@ ERROR: cannot drop inherited column "f1" alter table p1 drop column f1; -- c1.f1 is dropped now, since there is no local definition for it select f1 from c1; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1 from c1; ^ HINT: Perhaps you meant to reference the column "c1.f2". diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 64ea33aeae8..6874f5ae0d1 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -160,7 +160,7 @@ LINE 1: COPY x TO stdout WHERE a = 1; COPY x from stdin WHERE a = 50004; COPY x from stdin WHERE a > 60003; COPY x from stdin WHERE f > 60003; -ERROR: column "f" does not exist +ERROR: column or variable "f" does not exist LINE 1: COPY x from stdin WHERE f > 60003; ^ COPY x from stdin WHERE a = max(x.b); diff --git a/src/test/regress/expected/errors.out b/src/test/regress/expected/errors.out index 8c527474daf..e53ae451dfc 100644 --- a/src/test/regress/expected/errors.out +++ b/src/test/regress/expected/errors.out @@ -27,7 +27,7 @@ LINE 1: select * from nonesuch; ^ -- bad name in target list select nonesuch from pg_database; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select nonesuch from pg_database; ^ -- empty distinct list isn't OK @@ -37,17 +37,17 @@ LINE 1: select distinct from pg_database; ^ -- bad attribute name on lhs of operator select * from pg_database where nonesuch = pg_database.datname; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: select * from pg_database where nonesuch = pg_database.datna... ^ -- bad attribute name on rhs of operator select * from pg_database where pg_database.datname = nonesuch; -ERROR: column "nonesuch" does not exist +ERROR: column or variable "nonesuch" does not exist LINE 1: ...ect * from pg_database where pg_database.datname = nonesuch; ^ -- bad attribute name in select distinct on select distinct on (foobar) * from pg_database; -ERROR: column "foobar" does not exist +ERROR: column or variable "foobar" does not exist LINE 1: select distinct on (foobar) * from pg_database; ^ -- grouping with FOR UPDATE diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 9b2973694ff..d9f16aeb978 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6310,13 +6310,13 @@ LINE 1: select t2.uunique1 from HINT: Perhaps you meant to reference the column "t2.unique1". select uunique1 from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, suggest both at once -ERROR: column "uunique1" does not exist +ERROR: column or variable "uunique1" does not exist LINE 1: select uunique1 from ^ HINT: Perhaps you meant to reference the column "t1.unique1" or the column "t2.unique1". select ctid from tenk1 t1 join tenk2 t2 on t1.two = t2.two; -- error, need qualification -ERROR: column "ctid" does not exist +ERROR: column or variable "ctid" does not exist LINE 1: select ctid from ^ DETAIL: There are columns named "ctid", but they are in tables that cannot be referenced from this part of the query. @@ -7413,7 +7413,7 @@ lateral (select * from int8_tbl t1, -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a, (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7425,7 +7425,7 @@ LINE 1: select f1,g from int4_tbl a, (select a.f1 as g) ss; DETAIL: There is an entry for table "a", but it cannot be referenced from this part of the query. HINT: To reference that table, you must mark this subquery with LATERAL. select f1,g from int4_tbl a cross join (select f1 as g) ss; -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 1: select f1,g from int4_tbl a cross join (select f1 as g) ss; ^ DETAIL: There is a column named "f1" in table "a", but it cannot be referenced from this part of the query. @@ -7462,7 +7462,7 @@ LINE 1: select 1 from tenk1 a, lateral (select max(a.unique1) from i... create temp table xx1 as select f1 as x1, -f1 as x2 from int4_tbl; -- error, can't do this: update xx1 set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ... set x2 = f1 from (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. @@ -7482,7 +7482,7 @@ update xx1 set x2 = f1 from xx1, lateral (select * from int4_tbl where f1 = x1) ERROR: table name "xx1" specified more than once -- also errors: delete from xx1 using (select * from int4_tbl where f1 = x1) ss; -ERROR: column "x1" does not exist +ERROR: column or variable "x1" does not exist LINE 1: ...te from xx1 using (select * from int4_tbl where f1 = x1) ss; ^ DETAIL: There is a column named "x1" in table "xx1", but it cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/namespace.out b/src/test/regress/expected/namespace.out index dbbda72d395..d98793f92a7 100644 --- a/src/test/regress/expected/namespace.out +++ b/src/test/regress/expected/namespace.out @@ -23,7 +23,7 @@ BEGIN; SET search_path to public, test_ns_schema_1; CREATE SCHEMA test_ns_schema_2 CREATE VIEW abc_view AS SELECT c FROM abc; -ERROR: column "c" does not exist +ERROR: column or variable "c" does not exist LINE 2: CREATE VIEW abc_view AS SELECT c FROM abc; ^ COMMIT; diff --git a/src/test/regress/expected/numerology.out b/src/test/regress/expected/numerology.out index 9e23166fed1..672012e1461 100644 --- a/src/test/regress/expected/numerology.out +++ b/src/test/regress/expected/numerology.out @@ -314,7 +314,7 @@ NOTICE: i = 1002 NOTICE: i = 1003 -- error cases SELECT _100; -ERROR: column "_100" does not exist +ERROR: column or variable "_100" does not exist LINE 1: SELECT _100; ^ SELECT 100_; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 0a6945581bd..5c599ba312e 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -2598,7 +2598,7 @@ end; $$ language plpgsql; -- should fail: SQLSTATE and SQLERRM are only in defined EXCEPTION -- blocks select excpt_test1(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -2613,7 +2613,7 @@ begin end; $$ language plpgsql; -- should fail select excpt_test2(); -ERROR: column "sqlstate" does not exist +ERROR: column or variable "sqlstate" does not exist LINE 1: sqlstate ^ QUERY: sqlstate @@ -4675,7 +4675,7 @@ BEGIN RAISE NOTICE '%, %', r.roomno, r.comment; END LOOP; END$$; -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomn... ^ QUERY: SELECT rtrim(roomno) AS roomno, foo FROM Room ORDER BY roomno @@ -4717,7 +4717,7 @@ begin raise notice 'x = %', x; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -4729,7 +4729,7 @@ begin raise notice 'x = %, y = %', x, y; end; $$; -ERROR: column "x" does not exist +ERROR: column or variable "x" does not exist LINE 1: x + 1 ^ QUERY: x + 1 @@ -5769,7 +5769,7 @@ ALTER TABLE alter_table_under_transition_tables DROP column name; UPDATE alter_table_under_transition_tables SET id = id; -ERROR: column "name" does not exist +ERROR: column or variable "name" does not exist LINE 1: (SELECT string_agg(id || '=' || name, ',') FROM d) ^ QUERY: (SELECT string_agg(id || '=' || name, ',') FROM d) diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 77a7bf45c8a..e1635f686c2 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -214,7 +214,7 @@ select $1::int as col \bind 1 \g \bind 2 \g -- errors -- parse error SELECT foo \bind \g -ERROR: column "foo" does not exist +ERROR: column or variable "foo" does not exist LINE 1: SELECT foo ^ -- tcop error diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 3014d047fef..7378f95b17d 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1191,7 +1191,7 @@ drop rule rules_foorule on rules_foo; -- this should fail because f1 is not exposed for unqualified reference: create rule rules_foorule as on insert to rules_foo where f1 < 100 do instead insert into rules_foo2 values (f1); -ERROR: column "f1" does not exist +ERROR: column or variable "f1" does not exist LINE 2: do instead insert into rules_foo2 values (f1); ^ DETAIL: There are columns named "f1", but they are in tables that cannot be referenced from this part of the query. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 96283ed0b9a..5a66fc9ffab 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -310,7 +310,7 @@ ERROR: session variable "var1" doesn't exist LINE 1: LET var1 = pi(); ^ SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ -- should be ok @@ -522,7 +522,7 @@ SELECT v1; -- should fail, attribute doesn't exist LET v1.x = 10; -ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +ERROR: cannot assign to field "x" of column or variable "v1" because there is no such column in data type t1 LINE 1: LET v1.x = 10; ^ -- should fail, don't allow multi column query @@ -1070,7 +1070,7 @@ BEGIN; DROP VARIABLE var1; -- should fail SELECT var1; -ERROR: column "var1" does not exist +ERROR: column or variable "var1" does not exist LINE 1: SELECT var1; ^ ROLLBACK; diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out index 7f5757e89c4..b05b16d94bf 100644 --- a/src/test/regress/expected/transactions.out +++ b/src/test/regress/expected/transactions.out @@ -256,7 +256,7 @@ SELECT * FROM trans_barbaz; -- should have 1 BEGIN; SAVEPOINT one; SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ ROLLBACK TO SAVEPOINT one; @@ -305,7 +305,7 @@ BEGIN; SAVEPOINT one; INSERT INTO savepoints VALUES (5); SELECT trans_foo; -ERROR: column "trans_foo" does not exist +ERROR: column or variable "trans_foo" does not exist LINE 1: SELECT trans_foo; ^ COMMIT; diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index c73631a9a1d..5c0eacc5f15 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -922,7 +922,7 @@ ORDER BY q2,q1; -- This should fail, because q2 isn't a name of an EXCEPT output column SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1; -ERROR: column "q2" does not exist +ERROR: column or variable "q2" does not exist LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1... ^ DETAIL: There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query. -- 2.47.0 [text/x-patch] v20241120-0020-pg_restore-A-variable.patch (2.8K, 5-v20241120-0020-pg_restore-A-variable.patch) download | inline diff: From 7ecec2da6441efc61defeca0fa95730d8941d484 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 21 Jul 2024 17:04:41 +0200 Subject: [PATCH 20/21] pg_restore -A, --variable possibility to restore session variable specified by name --- doc/src/sgml/ref/pg_restore.sgml | 11 +++++++++++ src/bin/pg_dump/pg_restore.c | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index b8b27e1719e..7a3a4eabe25 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -106,6 +106,17 @@ PostgreSQL documentation </listitem> </varlistentry> + <varlistentry> + <term><option>-A <replaceable class="parameter">session_variable</replaceable></option></term> + <term><option>--variable=<replaceable class="parameter">session_variable</replaceable></option></term> + <listitem> + <para> + Restore a named session variable only. Multiple session variables may + be specified with multiple <option>-A</option> switches. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><option>-c</option></term> <term><option>--clean</option></term> diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index f2c1020d053..f916736d761 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -104,6 +104,7 @@ main(int argc, char **argv) {"trigger", 1, NULL, 'T'}, {"use-list", 1, NULL, 'L'}, {"username", 1, NULL, 'U'}, + {"variable", 1, NULL, 'A'}, {"verbose", 0, NULL, 'v'}, {"single-transaction", 0, NULL, '1'}, @@ -154,7 +155,7 @@ main(int argc, char **argv) } } - while ((c = getopt_long(argc, argv, "acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", + while ((c = getopt_long(argc, argv, "A:acCd:ef:F:h:I:j:lL:n:N:Op:P:RsS:t:T:U:vwWx1", cmdopts, NULL)) != -1) { switch (c) @@ -162,6 +163,11 @@ main(int argc, char **argv) case 'a': /* Dump data only */ opts->dataOnly = 1; break; + case 'A': /* vAriable */ + opts->selTypes = 1; + opts->selVariable = 1; + simple_string_list_append(&opts->variableNames, optarg); + break; case 'c': /* clean (i.e., drop) schema prior to create */ opts->dropSchema = 1; break; @@ -462,6 +468,7 @@ usage(const char *progname) printf(_("\nOptions controlling the restore:\n")); printf(_(" -a, --data-only restore only the data, no schema\n")); + printf(_(" -A, --variable=NAME restore named session variable\n")); printf(_(" -c, --clean clean (drop) database objects before recreating\n")); printf(_(" -C, --create create the target database\n")); printf(_(" -e, --exit-on-error exit on error, default is to continue\n")); -- 2.47.0 [text/x-patch] v20241120-0017-expression-with-session-variables-can-be-inlined.patch (4.2K, 6-v20241120-0017-expression-with-session-variables-can-be-inlined.patch) download | inline diff: From 81b90c04890571f1a219014077323c67260973fe Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 20:35:38 +0100 Subject: [PATCH 17/21] expression with session variables can be inlined There is not an reason why session variables should to block inlining. (of SQL functions). I can imagine some use cases like wrapping, and inlining significantly reduces an overhead of SQL functions. --- src/backend/optimizer/util/clauses.c | 37 ++++++++++++++----- .../regress/expected/session_variables.out | 7 ++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8cee732561d..f79937980ee 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -4744,8 +4744,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - (list_length(querytree->targetList) != 1) || - querytree->hasSessionVariables) + (list_length(querytree->targetList) != 1)) goto fail; /* If the function result is composite, resolve it */ @@ -4949,21 +4948,39 @@ substitute_actual_parameters_mutator(Node *node, { if (node == NULL) return NULL; + + + /* + * SQL functions can contain two different kind of params. The nodes with + * paramkind PARAM_EXTERN are related to function's arguments (and should + * be replaced in this step), because this is how we apply the function's + * arguments for an expression. + * + * The nodes with paramkind PARAM_VARIABLE are related to usage of session + * variables. The values of session variables are not passed to expression + * by expression arguments, so it should not be replaced here by + * function's arguments. + */ if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind != PARAM_EXTERN) + if (param->paramkind != PARAM_EXTERN && + param->paramkind != PARAM_VARIABLE) elog(ERROR, "unexpected paramkind: %d", (int) param->paramkind); - if (param->paramid <= 0 || param->paramid > context->nargs) - elog(ERROR, "invalid paramid: %d", param->paramid); - /* Count usage of parameter */ - context->usecounts[param->paramid - 1]++; + if (param->paramkind == PARAM_EXTERN) + { + if (param->paramid <= 0 || param->paramid > context->nargs) + elog(ERROR, "invalid paramid: %d", param->paramid); - /* Select the appropriate actual arg and replace the Param with it */ - /* We don't need to copy at this time (it'll get done later) */ - return list_nth(context->args, param->paramid - 1); + /* Count usage of parameter */ + context->usecounts[param->paramid - 1]++; + + /* Select the appropriate actual arg and replace the Param with it */ + /* We don't need to copy at this time (it'll get done later) */ + return list_nth(context->args, param->paramid - 1); + } } return expression_tree_mutator(node, substitute_actual_parameters_mutator, (void *) context); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 8df3a91c05e..96283ed0b9a 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -105,7 +105,6 @@ SELECT var1; ERROR: permission denied for session variable var1 SELECT sqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN @@ -212,10 +211,10 @@ SELECT sqlfx1(sqlfx2('Hello')); -- inlining is blocked EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); - QUERY PLAN ------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------- Result - Output: sqlfx1(sqlfx2('Hello'::character varying)) + Output: ((var1 || ', '::text) || ((var2 || ', '::text) || 'Hello'::text)) (2 rows) DROP FUNCTION sqlfx1(varchar); -- 2.47.0 [text/x-patch] v20241120-0021-variable-fence-syntax-support-and-variable-fence-usa.patch (15.5K, 7-v20241120-0021-variable-fence-syntax-support-and-variable-fence-usa.patch) download | inline diff: From 6a6fb7c5e733667600282f24583f3305a546247a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 19 Nov 2024 08:14:53 +0100 Subject: [PATCH 21/21] variable fence syntax support and variable fence usage guard support this patch introduces a concept of variable fence - syntax for variable reference `VARIABLE(varname)` that is not in collision with column reference. When variable fence usage guard warning is active, then usage variable without variable fence in the case, where there can be column references, the the warning is raised. initial implementation of variable fence --- src/backend/nodes/nodeFuncs.c | 6 ++ src/backend/parser/gram.y | 28 ++++++- src/backend/parser/parse_expr.c | 74 ++++++++++++++++++- src/backend/parser/parse_target.c | 7 ++ src/backend/utils/adt/ruleutils.c | 12 ++- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/nodes/parsenodes.h | 10 +++ src/include/nodes/primnodes.h | 2 + src/include/parser/parse_expr.h | 1 + .../regress/expected/session_variables.out | 56 ++++++++++++++ src/test/regress/sql/session_variables.sql | 35 +++++++++ 12 files changed, 234 insertions(+), 7 deletions(-) diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 8fc67f08d76..d9288cefc79 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1662,6 +1662,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4681,6 +4684,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af9d640c0e2..ad66eb450b4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -519,7 +519,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -879,7 +879,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15656,6 +15656,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17042,6 +17055,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 16bf52c9854..fecf22b8b9e 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -46,6 +46,7 @@ /* GUC parameters */ bool Transform_null_equals = false; bool session_variables_ambiguity_warning = false; +bool session_variables_use_fence_warning_guard = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -81,6 +82,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -112,7 +114,7 @@ static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); static Node *makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location); + char *attrname, bool fenced, int location); /* @@ -377,6 +379,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -1030,7 +1036,20 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) node = makeParamSessionVariable(pstate, varid, typid, typmod, collid, - attrname, cref->location); + attrname, false, cref->location); + + /* + * The variable is not inside variable's fence, so raise warning + * when variable fence guard is active and the query has FROM + * clause. + */ + if (session_variables_use_fence_warning_guard && pstate->p_rtable) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is not used inside variable fence", + NameListToString(cref->fields)), + errdetail("The collision of session variable' names and column names is possible."), + parser_errposition(pstate, cref->location))); } } } @@ -1073,7 +1092,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) static Node * makeParamSessionVariable(ParseState *pstate, Oid varid, Oid typid, int32 typmod, Oid collid, - char *attrname, int location) + char *attrname, bool fenced, int location) { Param *param; @@ -1081,6 +1100,7 @@ makeParamSessionVariable(ParseState *pstate, param->paramkind = PARAM_VARIABLE; param->paramvarid = varid; + param->paramvarfenced = fenced; param->paramtype = typid; param->paramtypmod = typmod; param->paramcollid = collid; @@ -1156,6 +1176,54 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, true, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 6af6b801ad4..9d4adc0f5e9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2038,6 +2038,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index e7f74947cde..e218c7118b8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8622,8 +8622,16 @@ get_parameter(Param *param, deparse_context *context) /* translate paramvarid to session variable name */ if (param->paramkind == PARAM_VARIABLE) { - appendStringInfo(context->buf, "%s", - generate_session_variable_name(param->paramvarid)); + if (param->paramvarfenced) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + } + else + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + } return; } diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index efb65b02380..100dd26b286 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1604,6 +1604,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_use_fence_warning_guard", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when variable is not used inside variable fence."), + NULL + }, + &session_variables_use_fence_warning_guard, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 992dda3c644..512fe9e334d 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -715,6 +715,7 @@ #default_transaction_deferrable = off #session_replication_role = 'origin' #session_variables_ambiguity_warning = off +#session_variables_use_fence_warning_guard = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index a4372f184df..97ed845e073 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -312,6 +312,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6174a5562c5..2e5665ffebd 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -388,6 +388,8 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of session variable if it is used */ Oid paramvarid; + /* true when variable is used inside an fence */ + bool paramvarfenced; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index e5aebf99b4a..d1f2e59d2dc 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -18,6 +18,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; extern PGDLLIMPORT bool session_variables_ambiguity_warning; +extern PGDLLIMPORT bool session_variables_use_fence_warning_guard; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 3e6b73684fd..66f1959a329 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1863,3 +1863,59 @@ SELECT tv; (1 row) DROP VARIABLE tv; +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; +CREATE VARIABLE a AS int; +LET a = 10; +CREATE TABLE test_table(a int, b int); +INSERT INTO test_table VALUES(20, 20); +-- no warning +SELECT a; + a +---- + 10 +(1 row) + +-- warning variable is shadowed +SELECT a, b FROM test_table; +WARNING: session variable "a" is shadowed +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a | b +----+---- + 20 | 20 +(1 row) + +-- no warning +SELECT variable(a) FROM test_table; + a +---- + 10 +(1 row) + +ALTER TABLE test_table DROP COLUMN a; +-- warning - variable fence is not used +SELECT a, b FROM test_table; +WARNING: session variable "a" is not used inside variable fence +LINE 1: SELECT a, b FROM test_table; + ^ +DETAIL: The collision of session variable' names and column names is possible. + a | b +----+---- + 10 | 20 +(1 row) + +-- no warning +SELECT variable(a), b FROM test_table; + a | b +----+---- + 10 | 20 +(1 row) + +DROP VARIABLE a; +DROP TABLE test_table; +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 68fc0cde44a..45510e2ea54 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1266,3 +1266,38 @@ COMMIT; SELECT tv; DROP VARIABLE tv; + +SET session_variables_ambiguity_warning TO on; +SET session_variables_use_fence_warning_guard TO on; +SET search_path TO 'public'; + +CREATE VARIABLE a AS int; +LET a = 10; + +CREATE TABLE test_table(a int, b int); + +INSERT INTO test_table VALUES(20, 20); + +-- no warning +SELECT a; + +-- warning variable is shadowed +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a) FROM test_table; + +ALTER TABLE test_table DROP COLUMN a; + +-- warning - variable fence is not used +SELECT a, b FROM test_table; + +-- no warning +SELECT variable(a), b FROM test_table; + +DROP VARIABLE a; +DROP TABLE test_table; + +SET session_variables_ambiguity_warning TO DEFAULT; +SET session_variables_use_fence_warning_guard TO DEFAULT; +SET search_path TO DEFAULT; -- 2.47.0 [text/x-patch] v20241120-0016-plpgsql-implementation-for-LET-statement.patch (14.2K, 8-v20241120-0016-plpgsql-implementation-for-LET-statement.patch) download | inline diff: From 21ffbc3fdde24bd5b6b3af2c898ec682cc76ef0b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sat, 20 Jan 2024 08:56:17 +0100 Subject: [PATCH 16/21] plpgsql implementation for LET statement PLpgSQL allows to call expression executor directly (for simple expression). This possibility strongly reduces overhead related to execution. Reimplementation of LET statement (inside PLpgSQL) allows to use this possibility, and strongly increase performance: CREATE VARIABLE svar int; DO $$ BEGIN FOR i IN 1..10000 LOOP LET svar = i; END LOOP; END; $$; From 120ms to 8ms (with assertions) (this is best case, but without it, the LET statement can be bottle neck). An alternative can be reimplementation of LET statement inside expression executor, and then SQL LET command can be executed in simple expression execution, but this increase an complexity of executor, but still the benefits is only inside plpgsql, so it is better to this optimization inside plpgsql. --- src/backend/executor/spi.c | 3 ++ src/backend/parser/analyze.c | 12 +++++ src/backend/parser/gram.y | 8 ++++ src/backend/parser/parser.c | 1 + src/include/nodes/parsenodes.h | 2 + src/include/parser/parser.h | 4 ++ src/pl/plpgsql/src/pl_exec.c | 55 +++++++++++++++++++++++ src/pl/plpgsql/src/pl_funcs.c | 24 ++++++++++ src/pl/plpgsql/src/pl_gram.y | 28 +++++++++++- src/pl/plpgsql/src/pl_reserved_kwlist.h | 1 + src/pl/plpgsql/src/pl_unreserved_kwlist.h | 1 + src/pl/plpgsql/src/plpgsql.h | 12 +++++ src/tools/pgindent/typedefs.list | 1 + 13 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 2fb2e73604e..5ae25d07ad7 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2991,6 +2991,9 @@ _SPI_error_callback(void *arg) case RAW_PARSE_PLPGSQL_ASSIGN3: errcontext("PL/pgSQL assignment \"%s\"", query); break; + case RAW_PARSE_PLPGSQL_LET: + errcontext("LET statement \"%s\"", query); + break; default: errcontext("SQL statement \"%s\"", query); break; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 496d75a494a..2edc51cfee9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -2014,6 +2014,18 @@ transformLetStmt(ParseState *pstate, LetStmt *stmt) stmt->query = (Node *) query; + /* + * Inside PL/pgSQL we don't want to execute LET statement as utility + * command, because it disallow to execute expression as simple + * expression. So for PL/pgSQL we have extra path, and we return SELECT. + * Then it can be executed by exec_eval_expr. Result is dirrectly assigned + * to target session variable inside PL/pgSQL LET statement handler. This + * is extra code, extra path, but possibility to get faster execution is + * too attractive. + */ + if (stmt->plpgsql_mode) + return query; + /* represent the command as a utility Query */ result = makeNode(Query); result->commandType = CMD_UTILITY; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6aea3bd35b2..bc8e25b1933 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -817,6 +817,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %token MODE_PLPGSQL_ASSIGN1 %token MODE_PLPGSQL_ASSIGN2 %token MODE_PLPGSQL_ASSIGN3 +%token MODE_PLPGSQL_LET /* Precedence: lowest to highest */ @@ -948,6 +949,13 @@ parse_toplevel: pg_yyget_extra(yyscanner)->parsetree = list_make1(makeRawStmt((Node *) n, @2)); } + | MODE_PLPGSQL_LET LetStmt + { + LetStmt *n = (LetStmt *) $2; + n->plpgsql_mode = true; + pg_yyget_extra(yyscanner)->parsetree = + list_make1(makeRawStmt((Node *) n, 0)); + } ; /* diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c index 118488c3f30..f9430a32919 100644 --- a/src/backend/parser/parser.c +++ b/src/backend/parser/parser.c @@ -62,6 +62,7 @@ raw_parser(const char *str, RawParseMode mode) [RAW_PARSE_PLPGSQL_ASSIGN1] = MODE_PLPGSQL_ASSIGN1, [RAW_PARSE_PLPGSQL_ASSIGN2] = MODE_PLPGSQL_ASSIGN2, [RAW_PARSE_PLPGSQL_ASSIGN3] = MODE_PLPGSQL_ASSIGN3, + [RAW_PARSE_PLPGSQL_LET] = MODE_PLPGSQL_LET, }; yyextra.have_lookahead = true; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7308c323574..bf820828dd9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2114,6 +2114,8 @@ typedef struct LetStmt NodeTag type; List *target; /* target variable */ Node *query; /* source expression */ + bool plpgsql_mode; /* true, when command will be executed + * (parsed) by plpgsql runtime */ int location; } LetStmt; diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h index be184ec5066..efbc76f0ad4 100644 --- a/src/include/parser/parser.h +++ b/src/include/parser/parser.h @@ -33,6 +33,9 @@ * RAW_PARSE_PLPGSQL_ASSIGNn: parse a PL/pgSQL assignment statement, * and return a one-element List containing a RawStmt node. "n" * gives the number of dotted names comprising the target ColumnRef. + * + * RAW_PARSE_PLPGSQL_LET: parse a LET statement, and return a + * one-element List containing a RawStmt node. */ typedef enum { @@ -42,6 +45,7 @@ typedef enum RAW_PARSE_PLPGSQL_ASSIGN1, RAW_PARSE_PLPGSQL_ASSIGN2, RAW_PARSE_PLPGSQL_ASSIGN3, + RAW_PARSE_PLPGSQL_LET, } RawParseMode; /* Values for the backslash_quote GUC */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..b5cb1bee223 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -22,6 +22,7 @@ #include "access/tupconvert.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/spi.h" #include "executor/tstoreReceiver.h" @@ -323,6 +324,8 @@ static int exec_stmt_commit(PLpgSQL_execstate *estate, PLpgSQL_stmt_commit *stmt); static int exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt); +static int exec_stmt_let(PLpgSQL_execstate *estate, + PLpgSQL_stmt_let *let); static void plpgsql_estate_setup(PLpgSQL_execstate *estate, PLpgSQL_function *func, @@ -2116,6 +2119,10 @@ exec_stmts(PLpgSQL_execstate *estate, List *stmts) rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + rc = exec_stmt_let(estate, (PLpgSQL_stmt_let *) stmt); + break; + default: /* point err_stmt to parent, since this one seems corrupt */ estate->err_stmt = save_estmt; @@ -4999,6 +5006,54 @@ exec_stmt_rollback(PLpgSQL_execstate *estate, PLpgSQL_stmt_rollback *stmt) return PLPGSQL_RC_OK; } +/* ---------- + * exec_stmt_let Evaluate an expression and + * put the result into a session variable. + * ---------- + */ +static int +exec_stmt_let(PLpgSQL_execstate *estate, PLpgSQL_stmt_let *stmt) +{ + bool isNull; + Oid valtype; + int32 valtypmod; + Datum value; + Oid varid; + + List *plansources; + CachedPlanSource *plansource; + + value = exec_eval_expr(estate, + stmt->expr, + &isNull, + &valtype, + &valtypmod); + + /* + * Oid of target session variable is stored in Query structure. It is + * safer to read this value directly from the plan than to hold this value + * in the plpgsql context, because it's not necessary to handle + * invalidation of the cached value. Next operations are read only without + * any allocations, so we can expect that retrieving varid from Query + * should be fast. + */ + plansources = SPI_plan_get_plan_sources(stmt->expr->plan); + if (list_length(plansources) != 1) + elog(ERROR, "unexpected length of plansources of query for LET statement"); + + plansource = (CachedPlanSource *) linitial(plansources); + if (list_length(plansource->query_list) != 1) + elog(ERROR, "unexpected length of plansource of query for LET statement"); + + varid = linitial_node(Query, plansource->query_list)->resultVariable; + if (!OidIsValid(varid)) + elog(ERROR, "oid of target session variable is not valid"); + + SetSessionVariableWithSecurityCheck(varid, value, isNull); + + return PLPGSQL_RC_OK; +} + /* ---------- * exec_assign_expr Put an expression's result into a variable. * ---------- diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index eeb7c4d7c04..1b39351db7f 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -288,6 +288,8 @@ plpgsql_stmt_typename(PLpgSQL_stmt *stmt) return "COMMIT"; case PLPGSQL_STMT_ROLLBACK: return "ROLLBACK"; + case PLPGSQL_STMT_LET: + return "LET"; } return "unknown"; @@ -370,6 +372,7 @@ static void free_perform(PLpgSQL_stmt_perform *stmt); static void free_call(PLpgSQL_stmt_call *stmt); static void free_commit(PLpgSQL_stmt_commit *stmt); static void free_rollback(PLpgSQL_stmt_rollback *stmt); +static void free_let(PLpgSQL_stmt_let *stmt); static void free_expr(PLpgSQL_expr *expr); @@ -459,6 +462,9 @@ free_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: free_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + free_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -713,6 +719,12 @@ free_getdiag(PLpgSQL_stmt_getdiag *stmt) { } +static void +free_let(PLpgSQL_stmt_let *stmt) +{ + free_expr(stmt->expr); +} + static void free_expr(PLpgSQL_expr *expr) { @@ -815,6 +827,7 @@ static void dump_perform(PLpgSQL_stmt_perform *stmt); static void dump_call(PLpgSQL_stmt_call *stmt); static void dump_commit(PLpgSQL_stmt_commit *stmt); static void dump_rollback(PLpgSQL_stmt_rollback *stmt); +static void dump_let(PLpgSQL_stmt_let *stmt); static void dump_expr(PLpgSQL_expr *expr); @@ -914,6 +927,9 @@ dump_stmt(PLpgSQL_stmt *stmt) case PLPGSQL_STMT_ROLLBACK: dump_rollback((PLpgSQL_stmt_rollback *) stmt); break; + case PLPGSQL_STMT_LET: + dump_let((PLpgSQL_stmt_let *) stmt); + break; default: elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type); break; @@ -1590,6 +1606,14 @@ dump_getdiag(PLpgSQL_stmt_getdiag *stmt) printf("\n"); } +static void +dump_let(PLpgSQL_stmt_let *stmt) +{ + dump_ind(); + dump_expr(stmt->expr); + printf("\n"); +} + static void dump_expr(PLpgSQL_expr *expr) { diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 8182ce28aa1..3f155c4f8a4 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -200,7 +200,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %type <stmt> stmt_return stmt_raise stmt_assert stmt_execsql %type <stmt> stmt_dynexecute stmt_for stmt_perform stmt_call stmt_getdiag %type <stmt> stmt_open stmt_fetch stmt_move stmt_close stmt_null -%type <stmt> stmt_commit stmt_rollback +%type <stmt> stmt_commit stmt_rollback stmt_let %type <stmt> stmt_case stmt_foreach_a %type <list> proc_exceptions @@ -307,6 +307,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt); %token <keyword> K_INTO %token <keyword> K_IS %token <keyword> K_LAST +%token <keyword> K_LET %token <keyword> K_LOG %token <keyword> K_LOOP %token <keyword> K_MERGE @@ -876,6 +877,8 @@ proc_stmt : pl_block ';' { $$ = $1; } | stmt_rollback { $$ = $1; } + | stmt_let + { $$ = $1; } ; stmt_perform : K_PERFORM @@ -992,6 +995,29 @@ stmt_assign : T_DATUM } ; +stmt_let : K_LET + { + PLpgSQL_stmt_let *new; + RawParseMode pmode; + + pmode = RAW_PARSE_PLPGSQL_LET; + + new = palloc0(sizeof(PLpgSQL_stmt_let)); + new->cmd_type = PLPGSQL_STMT_LET; + new->lineno = plpgsql_location_to_lineno(@1); + new->stmtid = ++plpgsql_curr_compile->nstatements; + + /* push back the head name to include it in the stmt */ + plpgsql_push_back_token(K_LET); + new->expr = read_sql_construct(';', 0, 0, ";", + pmode, + false, true, + NULL, NULL); + + $$ = (PLpgSQL_stmt *)new; + } + ; + stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' { PLpgSQL_stmt_getdiag *new; diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h index d338e9f6374..be94aa751a4 100644 --- a/src/pl/plpgsql/src/pl_reserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h @@ -40,6 +40,7 @@ PG_KEYWORD("from", K_FROM) PG_KEYWORD("if", K_IF) PG_KEYWORD("in", K_IN) PG_KEYWORD("into", K_INTO) +PG_KEYWORD("let", K_LET) PG_KEYWORD("loop", K_LOOP) PG_KEYWORD("not", K_NOT) PG_KEYWORD("null", K_NULL) diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h index 670b4cf0c1e..602628d7d2a 100644 --- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h @@ -69,6 +69,7 @@ PG_KEYWORD("info", K_INFO) PG_KEYWORD("insert", K_INSERT) PG_KEYWORD("is", K_IS) PG_KEYWORD("last", K_LAST) +PG_KEYWORD("let", K_LET) PG_KEYWORD("log", K_LOG) PG_KEYWORD("merge", K_MERGE) PG_KEYWORD("message", K_MESSAGE) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 50c3b28472b..bb400629adb 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -128,6 +128,7 @@ typedef enum PLpgSQL_stmt_type PLPGSQL_STMT_CALL, PLPGSQL_STMT_COMMIT, PLPGSQL_STMT_ROLLBACK, + PLPGSQL_STMT_LET, } PLpgSQL_stmt_type; /* @@ -520,6 +521,17 @@ typedef struct PLpgSQL_stmt_assign PLpgSQL_expr *expr; } PLpgSQL_stmt_assign; +/* + * Let statement + */ +typedef struct PLpgSQL_stmt_let +{ + PLpgSQL_stmt_type cmd_type; + int lineno; + unsigned int stmtid; + PLpgSQL_expr *expr; +} PLpgSQL_stmt_let; + /* * PERFORM statement */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 64ddf589b3c..fe60f2916f4 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1890,6 +1890,7 @@ PLpgSQL_stmt_forq PLpgSQL_stmt_fors PLpgSQL_stmt_getdiag PLpgSQL_stmt_if +PLpgSQL_stmt_let PLpgSQL_stmt_loop PLpgSQL_stmt_open PLpgSQL_stmt_perform -- 2.47.0 [text/x-patch] v20241120-0014-allow-read-an-value-of-session-variable-directly-fro.patch (13.3K, 9-v20241120-0014-allow-read-an-value-of-session-variable-directly-fro.patch) download | inline diff: From b2336b4ebcfcbc076c74a81dcd070762b2e8af32 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:07:03 +0100 Subject: [PATCH 14/21] allow read an value of session variable directly from expression executor The expression executor can be called directly (not inside query executor) when expression is evaluated as simple expression in plpgsql or when expression is an argument of CALL or EXECUTE statements. For these cases we need to allow direct access to content of session variables from executor. We can do it, because we know so the expression is not evaluated under query and cannot be evaluated inside parallel worker. This patch enables session variables for simple expression evaluation. This patch enables usage session variables in arguments of CALL stmt. --- src/backend/commands/prepare.c | 8 -- src/backend/executor/execExpr.c | 82 ++++++++++++++----- src/backend/executor/execExprInterp.c | 16 ++++ src/backend/jit/llvm/llvmjit_expr.c | 6 ++ src/backend/parser/analyze.c | 8 -- src/include/executor/execExpr.h | 12 ++- .../src/expected/plpgsql_session_variable.out | 3 +- src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 26 +++--- src/test/regress/sql/session_variables.sql | 12 +-- 10 files changed, 117 insertions(+), 59 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 3735b5554e0..e8c375df198 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,14 +338,6 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } - /* - * The arguments of EXECUTE are evaluated by a direct expression - * executor call. This mode doesn't support session variables yet. - * It will be enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 6dc8c562bec..845bdd1ae85 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1005,25 +1005,69 @@ ExecInitExprRec(Expr *node, ExprState *state, es_num_session_variables = state->parent->state->es_num_session_variables; } - Assert(es_session_variables); - - /* parameter sanity checks */ - if (param->paramid >= es_num_session_variables) - elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); - - var = &es_session_variables[param->paramid]; - - if (var->typid != param->paramtype) - elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); - - /* - * In this case, pass the value like a - * constant. - */ - scratch.opcode = EEOP_CONST; - scratch.d.constval.value = var->value; - scratch.d.constval.isnull = var->isnull; - ExprEvalPushStep(state, &scratch); + if (es_session_variables) + { + /* + * This path is used when expression is + * evaluated inside query evaluation. For + * ensuring of immutability of session variable + * inside query we use special buffer with copy + * of used session variables. This method helps + * with parallel execution too. + */ + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + else + { + AclResult aclresult; + Oid varid = param->paramvarid; + Oid vartype = param->paramtype; + + Assert(!IsParallelWorker()); + + /* + * In some cases (plpgsql's simple expression + * evaluation or evaluation of CALL arguments), + * the expression executor is called directly. + * We can allow direct access to session + * variables (copy only), because we know, so + * outside is not any query (and expression + * cannot be executed parallel). + * + * In this case we should to do aclcheck, + * because usual aclcheck from + * standard_ExecutorStart is not executed in + * this case. Fortunately it is just once per + * transaction. + */ + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + scratch.opcode = EEOP_PARAM_VARIABLE; + scratch.d.vparam.varid = varid; + scratch.d.vparam.vartype = vartype; + ExprEvalPushStep(state, &scratch); + } } break; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 30c5a19aad6..0a154b97d6e 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -59,6 +59,7 @@ #include "access/heaptoast.h" #include "catalog/pg_type.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -450,6 +451,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_PARAM_EXEC, &&CASE_EEOP_PARAM_EXTERN, &&CASE_EEOP_PARAM_CALLBACK, + &&CASE_EEOP_PARAM_VARIABLE, &&CASE_EEOP_PARAM_SET, &&CASE_EEOP_CASE_TESTVAL, &&CASE_EEOP_MAKE_READONLY, @@ -1099,6 +1101,20 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_PARAM_VARIABLE) + { + /* + * Direct access to session variable (without buffering). Because + * returned value can be used (without an assignement) after the + * referenced session variables is updated, we have to use an copy + * of stored value every time. + */ + *op->resvalue = GetSessionVariableWithTypeCheck(op->d.vparam.varid, + op->resnull, + op->d.vparam.vartype); + EEO_NEXT(); + } + EEO_CASE(EEOP_PARAM_SET) { /* out of line, unlikely to matter performance-wise */ diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 9cec17544b8..e8cc54351ed 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -1136,6 +1136,12 @@ llvm_compile_expr(ExprState *state) break; } + case EEOP_PARAM_VARIABLE: + build_EvalXFunc(b, mod, "ExecEvalParamVariable", + v_state, op, v_econtext); + LLVMBuildBr(b, opblocks[opno + 1]); + break; + case EEOP_PARAM_SET: build_EvalXFunc(b, mod, "ExecEvalParamSet", v_state, op, v_econtext); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 9dfd435128a..496d75a494a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3470,14 +3470,6 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); - /* - * The arguments of CALL statement are evaluated by a direct expression - * executor call. This path is unsupported yet, so block it. It will be - * enabled later. - */ - if (pstate->p_hasSessionVariables) - elog(ERROR, "session variable cannot be used as an argument"); - assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index cd97dfa0624..72b54b3a33f 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -156,10 +156,11 @@ typedef enum ExprEvalOp EEOP_BOOLTEST_IS_FALSE, EEOP_BOOLTEST_IS_NOT_FALSE, - /* evaluate PARAM_EXEC/EXTERN parameters */ + /* evaluate PARAM_EXEC/EXTERN/VARIABLE parameters */ EEOP_PARAM_EXEC, EEOP_PARAM_EXTERN, EEOP_PARAM_CALLBACK, + EEOP_PARAM_VARIABLE, /* set PARAM_EXEC value */ EEOP_PARAM_SET, @@ -409,6 +410,13 @@ typedef struct ExprEvalStep Oid paramtype; /* OID of parameter's datatype */ } cparam; + /* for EEOP_PARAM_VARIABLE */ + struct + { + Oid varid; /* OID of assigned variable */ + Oid vartype; /* OID of parameter's datatype */ + } vparam; + /* for EEOP_CASE_TESTVAL/DOMAIN_TESTVAL */ struct { @@ -831,6 +839,8 @@ extern void ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalParamVariable(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalCoerceViaIOSafe(ExprState *state, ExprEvalStep *op); extern void ExecEvalSQLValueFunction(ExprState *state, ExprEvalStep *op); extern void ExecEvalCurrentOfExpr(ExprState *state, ExprEvalStep *op); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index a6523f7afe2..ecac64fcfb4 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -211,8 +211,7 @@ SET ROLE TO regress_var_exec_role; -- should fail SELECT var_read_func(); ERROR: permission denied for session variable plpgsql_sv_var1 -CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" -PL/pgSQL function var_read_func() line 3 at RAISE +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE SET ROLE TO DEFAULT; SET ROLE TO regress_var_owner_role; GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 1361a8a8480..86c5bd324a9 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,8 +8099,7 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations || - query->hasSessionVariables) + query->setOperations) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a8520e5579d..2f1708f7c84 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -108,8 +108,7 @@ ERROR: permission denied for session variable var1 CONTEXT: SQL function "sqlfx" statement 1 SELECT plpgsqlfx(20); ERROR: permission denied for session variable var1 -CONTEXT: PL/pgSQL expression "$1 + var1" -PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +CONTEXT: PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN -- should be ok SELECT sqlfx_sd(20); sqlfx_sd @@ -279,27 +278,28 @@ $$; NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); -ERROR: session variable cannot be used as an argument +NOTICE: 1 DO $$ BEGIN CALL p(v); END $$; -ERROR: session variable cannot be used as an argument -CONTEXT: SQL statement "CALL p(v)" -PL/pgSQL function inline_code_block line 1 at CALL +NOTICE: 1 DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); -ERROR: session variable cannot be used as an argument + ?column? +---------- + 20 +(1 row) + DEALLOCATE ptest; DROP VARIABLE v; -- test search path diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index ac8e2cb0943..fcd783e966c 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -207,13 +207,14 @@ $$; DROP VARIABLE var1; DROP VARIABLE var2; --- CALL statement is not supported yet --- requires direct access to session variable from expression executor +-- CALL statement is supported CREATE VARIABLE v int; +LET v = 1; + CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; --- should not crash (but is not supported yet) +-- should to work CALL p(v); DO $$ BEGIN CALL p(v); END $$; @@ -221,13 +222,12 @@ DO $$ BEGIN CALL p(v); END $$; DROP PROCEDURE p(int); DROP VARIABLE v; --- EXECUTE statement is not supported yet --- requires direct access to session variable from expression executor +-- EXECUTE statement CREATE VARIABLE v int; LET v = 20; PREPARE ptest(int) AS SELECT $1; --- should fail +-- should be ok EXECUTE ptest(v); DEALLOCATE ptest; -- 2.47.0 [text/x-patch] v20241120-0015-allow-parallel-execution-queries-with-session-variab.patch (11.9K, 10-v20241120-0015-allow-parallel-execution-queries-with-session-variab.patch) download | inline diff: From 3b42b71ae00fe9b6480b23e7492c529e28e33032 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:08:17 +0100 Subject: [PATCH 15/21] allow parallel execution queries with session variables --- doc/src/sgml/parallel.sgml | 6 - src/backend/executor/execMain.c | 14 +- src/backend/executor/execParallel.c | 147 +++++++++++++++++- src/backend/optimizer/util/clauses.c | 18 +-- .../regress/expected/session_variables.out | 12 +- 5 files changed, 171 insertions(+), 26 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 683dede6adc..1ce9abf86f5 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,12 +515,6 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> - - <listitem> - <para> - Plan nodes that use a session variable. - </para> - </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 21e938a6227..783716c4e57 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -212,7 +212,19 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) * be changed inside query execution time, and then a reference to * previously returned value can be corrupted). */ - if (queryDesc->plannedstmt->sessionVariables) + if (queryDesc->num_session_variables > 0) + { + /* + * When a parallel query needs to access query parameters (including + * related session variables), then related session variables are + * restored (deserialized) in queryDesc already. So just push pointer + * of this array to executor's estate. + */ + Assert(IsParallelWorker()); + estate->es_session_variables = queryDesc->session_variables; + estate->es_num_session_variables = queryDesc->num_session_variables; + } + else if (queryDesc->plannedstmt->sessionVariables) { ListCell *lc; int nSessionVariables; diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c index bfb3419efb7..cb434e2768e 100644 --- a/src/backend/executor/execParallel.c +++ b/src/backend/executor/execParallel.c @@ -12,8 +12,9 @@ * workers and ensuring that their state generally matches that of the * leader; see src/backend/access/transam/README.parallel for details. * However, we must save and restore relevant executor state, such as - * any ParamListInfo associated with the query, buffer/WAL usage info, and - * the actual plan to be passed down to the worker. + * any ParamListInfo associated with the query, buffer/WAL usage info, + * session variables buffer, and the actual plan to be passed down to + * the worker. * * IDENTIFICATION * src/backend/executor/execParallel.c @@ -64,6 +65,7 @@ #define PARALLEL_KEY_QUERY_TEXT UINT64CONST(0xE000000000000008) #define PARALLEL_KEY_JIT_INSTRUMENTATION UINT64CONST(0xE000000000000009) #define PARALLEL_KEY_WAL_USAGE UINT64CONST(0xE00000000000000A) +#define PARALLEL_KEY_SESSION_VARIABLES UINT64CONST(0xE00000000000000B) #define PARALLEL_TUPLE_QUEUE_SIZE 65536 @@ -138,6 +140,12 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate, /* Helper function that runs in the parallel worker. */ static DestReceiver *ExecParallelGetReceiver(dsm_segment *seg, shm_toc *toc); +/* Helper functions that can pass values of session variables */ +static Size EstimateSessionVariables(EState *estate); +static void SerializeSessionVariables(EState *estate, char **start_address); +static SessionVariableValue *RestoreSessionVariables(char **start_address, + int *num_session_variables); + /* * Create a serialized representation of the plan to be sent to each worker. */ @@ -596,6 +604,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, char *pstmt_data; char *pstmt_space; char *paramlistinfo_space; + char *session_variables_space; BufferUsage *bufusage_space; WalUsage *walusage_space; SharedExecutorInstrumentation *instrumentation = NULL; @@ -605,6 +614,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, int instrumentation_len = 0; int jit_instrumentation_len = 0; int instrument_offset = 0; + int session_variables_len = 0; Size dsa_minsize = dsa_minimum_size(); char *query_string; int query_len; @@ -660,6 +670,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_estimate_chunk(&pcxt->estimator, paramlistinfo_len); shm_toc_estimate_keys(&pcxt->estimator, 1); + /* Estimate space for serialized session variables. */ + session_variables_len = EstimateSessionVariables(estate); + shm_toc_estimate_chunk(&pcxt->estimator, session_variables_len); + shm_toc_estimate_keys(&pcxt->estimator, 1); + /* * Estimate space for BufferUsage. * @@ -761,6 +776,11 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, shm_toc_insert(pcxt->toc, PARALLEL_KEY_PARAMLISTINFO, paramlistinfo_space); SerializeParamList(estate->es_param_list_info, ¶mlistinfo_space); + /* Store serialized session variables. */ + session_variables_space = shm_toc_allocate(pcxt->toc, session_variables_len); + shm_toc_insert(pcxt->toc, PARALLEL_KEY_SESSION_VARIABLES, session_variables_space); + SerializeSessionVariables(estate, &session_variables_space); + /* Allocate space for each worker's BufferUsage; no need to initialize. */ bufusage_space = shm_toc_allocate(pcxt->toc, mul_size(sizeof(BufferUsage), pcxt->nworkers)); @@ -1411,6 +1431,7 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) SharedJitInstrumentation *jit_instrumentation; int instrument_options = 0; void *area_space; + char *sessionvariable_space; dsa_area *area; ParallelWorkerContext pwcxt; @@ -1436,6 +1457,14 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) area_space = shm_toc_lookup(toc, PARALLEL_KEY_DSA, false); area = dsa_attach_in_place(area_space, seg); + /* Reconstruct session variables. */ + sessionvariable_space = shm_toc_lookup(toc, + PARALLEL_KEY_SESSION_VARIABLES, + false); + queryDesc->session_variables = + RestoreSessionVariables(&sessionvariable_space, + &queryDesc->num_session_variables); + /* Start up the executor */ queryDesc->plannedstmt->jitFlags = fpes->jit_flags; ExecutorStart(queryDesc, fpes->eflags); @@ -1504,3 +1533,117 @@ ParallelQueryMain(dsm_segment *seg, shm_toc *toc) FreeQueryDesc(queryDesc); receiver->rDestroy(receiver); } + +/* + * Estimate the amount of space required to serialize a session variable. + */ +static Size +EstimateSessionVariables(EState *estate) +{ + int i; + Size sz = sizeof(int); + + if (estate->es_session_variables == NULL) + return sz; + + for (i = 0; i < estate->es_num_session_variables; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + + typeOid = svarval->typid; + + sz = add_size(sz, sizeof(Oid)); /* space for type OID */ + + /* space for datum/isnull */ + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + sz = add_size(sz, + datumEstimateSpace(svarval->value, svarval->isnull, typByVal, typLen)); + } + + return sz; +} + +/* + * Serialize a session variables buffer into caller-provided storage. + * + * We write the number of parameters first, as a 4-byte integer, and then + * write details for each parameter in turn. The details for each parameter + * consist of a 4-byte type OID, and then the datum as serialized by + * datumSerialize(). The caller is responsible for ensuring that there is + * enough storage to store the number of bytes that will be written; use + * EstimateSessionVariables to find out how many will be needed. + * *start_address is updated to point to the byte immediately following those + * written. + * + * RestoreSessionVariables can be used to recreate a session variable buffer + * based on the serialized representation; + */ +static void +SerializeSessionVariables(EState *estate, char **start_address) +{ + int nparams; + int i; + + /* Write number of parameters. */ + nparams = estate->es_num_session_variables; + memcpy(*start_address, &nparams, sizeof(int)); + *start_address += sizeof(int); + + /* Write each parameter in turn. */ + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval; + Oid typeOid; + int16 typLen; + bool typByVal; + + svarval = &estate->es_session_variables[i]; + typeOid = svarval->typid; + + /* Write type OID. */ + memcpy(*start_address, &typeOid, sizeof(Oid)); + *start_address += sizeof(Oid); + + Assert(OidIsValid(typeOid)); + get_typlenbyval(typeOid, &typLen, &typByVal); + + datumSerialize(svarval->value, svarval->isnull, typByVal, typLen, + start_address); + } +} + +static SessionVariableValue * +RestoreSessionVariables(char **start_address, int *num_session_variables) +{ + SessionVariableValue *session_variables; + int i; + int nparams; + + memcpy(&nparams, *start_address, sizeof(int)); + *start_address += sizeof(int); + + *num_session_variables = nparams; + session_variables = (SessionVariableValue *) + palloc(nparams * sizeof(SessionVariableValue)); + + for (i = 0; i < nparams; i++) + { + SessionVariableValue *svarval = &session_variables[i]; + + /* Read type OID. */ + memcpy(&svarval->typid, *start_address, sizeof(Oid)); + *start_address += sizeof(Oid); + + /* Read datum/isnull. */ + svarval->value = datumRestore(start_address, &svarval->isnull); + } + + return session_variables; +} diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index a5452c0c9e4..8cee732561d 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -924,25 +924,19 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) /* * We can't pass Params to workers at the moment either, so they are also - * parallel-restricted, unless they are PARAM_EXTERN Params or are - * PARAM_EXEC Params listed in safe_param_ids, meaning they could be - * either generated within workers or can be computed by the leader and - * then their value can be passed to workers. + * parallel-restricted, unless they are PARAM_EXTERN or PARAM_VARIABLE + * Params or are PARAM_EXEC Params listed in safe_param_ids, meaning they + * could be either generated within workers or can be computed by the + * leader and then their value can be passed to workers. */ else if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind == PARAM_EXTERN) + if (param->paramkind == PARAM_EXTERN || + param->paramkind == PARAM_VARIABLE) return false; - /* we don't support passing session variables to workers */ - if (param->paramkind == PARAM_VARIABLE) - { - if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) - return true; - } - if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2f1708f7c84..8df3a91c05e 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -644,12 +644,14 @@ SELECT count(*) FROM svar_test WHERE a%10 = zero; -- parallel execution is not supported yet EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; - QUERY PLAN ------------------------------------ + QUERY PLAN +-------------------------------------------- Aggregate - -> Seq Scan on svar_test - Filter: ((a % 10) = zero) -(3 rows) + -> Gather + Workers Planned: 2 + -> Parallel Seq Scan on svar_test + Filter: ((a % 10) = zero) +(5 rows) LET zero = (SELECT count(*) FROM svar_test); -- result should be 1000 -- 2.47.0 [text/x-patch] v20241120-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch (35.6K, 11-v20241120-0013-Implementation-of-NOT-NULL-and-IMMUTABLE-clauses.patch) download | inline diff: From 047e8a0fbb6e5739d6625e706a4ee4b61d018c52 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:05:06 +0100 Subject: [PATCH 13/21] Implementation of NOT NULL and IMMUTABLE clauses Almost trivial patch - psql, pg_dump support --- doc/src/sgml/catalogs.sgml | 20 +++ doc/src/sgml/plpgsql.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 32 +++- src/backend/catalog/pg_variable.c | 8 + src/backend/commands/session_variable.c | 69 +++++++- src/backend/parser/gram.y | 41 +++-- src/bin/pg_dump/pg_dump.c | 19 ++- src/bin/pg_dump/pg_dump.h | 2 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++ src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/pg_variable.h | 6 + src/include/nodes/parsenodes.h | 2 + src/test/regress/expected/psql.out | 36 ++-- .../regress/expected/session_variables.out | 155 +++++++++++++++++- src/test/regress/sql/session_variables.sql | 122 ++++++++++++++ 16 files changed, 523 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 124902c4e44..9a8886fc69a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9842,6 +9842,26 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <row> <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnotnull</structfield> <type>boolean</type> + </para> + <para> + True if the session variable doesn't allow null values. The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varisimmutable</structfield> <type>boolean</type> + </para> + <para> + True if the variable is <link linkend="sql-createvariable-immutable">immutable</link> (cannot be modified). + The default value is false. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vareoxaction</structfield> <type>char</type> <structfield>varxactendaction</structfield> <type>char</type> </para> <para> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index bfbff9ab74e..e704e05a991 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6044,7 +6044,8 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; You can consider translating an Oracle package into a schema in <productname>PostgreSQL</productname>. Package functions and procedures would then become functions and procedures in that schema, and package - variables could be translated to session variables in that schema. + variables could be translated to session variables or immutable session + variables in that schema. (see <xref linkend="ddl-session-variables"/>). </para> </sect3> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 8187c18e28b..00938743c0d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,8 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] +CREATE [ { TEMPORARY | TEMP } ] [ IMMUTABLE ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ NOT NULL ] [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -65,6 +65,22 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <variablelist> + <varlistentry id="sql-createvariable-immutable"> + <term><literal>IMMUTABLE</literal></term> + <listitem> + <para> + The assigned value of the session variable can not be changed. + Only if the session variable doesn't have a default value, a single + initialization is allowed using the <command>LET</command> command. Once + done, no further change is allowed until end of transaction + if the session variable was created with clause <literal>ON TRANSACTION + END RESET</literal>, or until reset of all session variables by + <command>DISCARD VARIABLES</command>, or until reset of all session + objects by command <command>DISCARD ALL</command>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-if-not-exists"> <term><literal>IF NOT EXISTS</literal></term> <listitem> @@ -105,6 +121,18 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-not-null"> + <term><literal>NOT NULL</literal></term> + <listitem> + <para> + The <literal>NOT NULL</literal> clause forbids setting the session + variable to a null value. A session variable created as NOT NULL + should not to have a declared default value, and if the variable + has not assigned value, then the reading of this variable fails. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-default"> <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> <listitem> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 36de12587c0..f261e3fd770 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -40,6 +40,8 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -55,6 +57,8 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + bool not_null, + bool is_immutable, Node *varDefexpr, VariableXactEndAction varXactEndAction) { @@ -117,6 +121,8 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varnotnull - 1] = BoolGetDatum(not_null); + values[Anum_pg_variable_varisimmutable - 1] = BoolGetDatum(is_immutable); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); if (varDefexpr) @@ -259,6 +265,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + stmt->not_null, + stmt->is_immutable, cooked_default, stmt->XactEndAction); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0a1c326e72e..7f34f93b542 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -84,6 +84,9 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool not_null; + bool is_immutable; + bool reset_at_eox; /* @@ -102,6 +105,9 @@ typedef struct SVariableData bool is_valid; uint32 hashvalue; /* used for pairing sinval message */ + + /* true, when the value is already set, and cannot be changed more */ + bool protect_value; } SVariableData; typedef SVariableData *SVariable; @@ -563,6 +569,23 @@ eval_assign_defexpr(SVariable svar, HeapTuple tup) MemoryContextSwitchTo(oldcxt); } + else + { + /* + * Raise an error if this is a NOT NULL variable but the result of + * DEFAULT expression is NULL. + */ + if (svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)), + errdetail("The result of DEFAULT expression is NULL."))); + } + + if (svar->is_immutable) + svar->protect_value = true; FreeExecutorState(estate); } @@ -593,6 +616,9 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + svar->not_null = varform->varnotnull; + svar->is_immutable = varform->varisimmutable; + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; @@ -622,9 +648,31 @@ setup_session_variable(SVariable svar, Oid varid, bool is_write) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); - if (!is_write) + svar->protect_value = false; + + /* + * When the variable is marked as IMMUTABLE, we prefer to evaluate + * possible DEFAULT before write op. In this case we want to protect + * default value against any overwrite. + */ + if (!is_write || + svar->is_immutable) + { eval_assign_defexpr(svar, tup); + /* + * Raise an error if this is a NOT NULL variable without default + * expression. + */ + if (svar->isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("The session variable was not initialized yet."))); + } + ReleaseSysCache(tup); } @@ -644,6 +692,21 @@ set_session_variable(SVariable svar, Datum value, bool isnull) Assert(svar); Assert(!isnull || value == (Datum) 0); + if (svar->protect_value) + ereport(ERROR, + (errcode(ERRCODE_ERROR_IN_ASSIGNMENT), + errmsg("session variable \"%s.%s\" is declared IMMUTABLE", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + + /* don't allow assignment of null to NOT NULL variable */ + if (isnull && svar->not_null) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value is not allowed for NOT NULL session variable \"%s.%s\"", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid)))); + /* * Use typbyval, typbylen from session variable only when they are * trustworthy (the invalidation message was not accepted for this @@ -684,6 +747,10 @@ set_session_variable(SVariable svar, Datum value, bool isnull) svar->value = newval; svar->isnull = isnull; + + /* don't allow more changes of value when variable is IMMUTABLE */ + if (svar->is_immutable) + svar->protect_value = true; } /* diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b8e33ed19db..6aea3bd35b2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -668,6 +668,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <boolean> OptNotNull OptImmutable /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -5231,27 +5232,31 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + CREATE OptTemp OptImmutable VARIABLE qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $4->relpersistence = $2; - n->variable = $4; - n->typeName = $6; - n->collClause = (CollateClause *) $7; - n->defexpr = $8; - n->XactEndAction = $9; + $5->relpersistence = $2; + n->is_immutable = $3; + n->variable = $5; + n->typeName = $7; + n->collClause = (CollateClause *) $8; + n->not_null = $9; + n->defexpr = $10; + n->XactEndAction = $11; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption + | CREATE OptTemp OptImmutable VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptNotNull OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - $7->relpersistence = $2; - n->variable = $7; - n->typeName = $9; - n->collClause = (CollateClause *) $10; - n->defexpr = $11; - n->XactEndAction = $12; + $8->relpersistence = $2; + n->is_immutable = $3; + n->variable = $8; + n->typeName = $10; + n->collClause = (CollateClause *) $11; + n->not_null = $12; + n->defexpr = $13; + n->XactEndAction = $14; n->if_not_exists = true; $$ = (Node *) n; } @@ -5271,6 +5276,14 @@ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } ; +OptNotNull: NOT NULL_P { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + +OptImmutable: IMMUTABLE { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c138994304a..59f4103ca4f 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5437,6 +5437,8 @@ getVariables(Archive *fout) int i_varxactendaction; int i_varowner; int i_varcollation; + int i_varnotnull; + int i_varisimmutable; int i_varacl; int i_acldefault; int i, @@ -5459,6 +5461,8 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " v.varnotnull,\n" + " v.varisimmutable,\n" " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" @@ -5479,6 +5483,8 @@ getVariables(Archive *fout) i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); + i_varnotnull = PQfnumber(res, "varnotnull"); + i_varisimmutable = PQfnumber(res, "varisimmutable"); i_varowner = PQfnumber(res, "varowner"); i_varacl = PQfnumber(res, "varacl"); @@ -5505,6 +5511,8 @@ getVariables(Archive *fout) pg_strdup(PQgetvalue(res, i, i_varxactendaction)); varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + varinfo[i].varnotnull = *(PQgetvalue(res, i, i_varnotnull)) == 't'; + varinfo[i].varisimmutable = *(PQgetvalue(res, i, i_varisimmutable)) == 't'; varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); @@ -5551,7 +5559,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) const char *vartypname; const char *vardefexpr; const char *varxactendaction; + const char *varisimmutable; Oid varcollation; + bool varnotnull; /* skip if not to be dumped */ if (!varinfo->dobj.dump || dopt->dataOnly) @@ -5565,12 +5575,14 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; + varnotnull = varinfo->varnotnull; + varisimmutable = varinfo->varisimmutable ? "IMMUTABLE " : ""; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", qualvarname); - appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", - qualvarname, vartypname); + appendPQExpBuffer(query, "CREATE %sVARIABLE %s AS %s", + varisimmutable, qualvarname, vartypname); if (OidIsValid(varcollation)) { @@ -5582,6 +5594,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (varnotnull) + appendPQExpBuffer(query, " NOT NULL"); + if (vardefexpr) appendPQExpBuffer(query, " DEFAULT %s", vardefexpr); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 8ed83979ec9..f7921f942d0 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -717,6 +717,8 @@ typedef struct _VariableInfo char *initrvaracl; Oid varcollation; const char *rolname; /* name of owner, or empty string */ + bool varnotnull; + bool varisimmutable; } VariableInfo; /* diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index f58ede5334a..47cb59356e0 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4041,6 +4041,42 @@ my %tests = ( }, }, + 'CREATE IMMUTABLE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable5 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE IMMUTABLE VARIABLE test_variable NOT NULL DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10', + regexp => qr/^ + \QCREATE IMMUTABLE VARIABLE dump_test.variable7 AS integer NOT NULL DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dc18da350eb..cc69f07f1f9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,8 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " NOT v.varnotnull as \"%s\",\n" + " NOT v.varisimmutable as \"%s\",\n" " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" @@ -5237,6 +5239,8 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Nullable"), + gettext_noop("Mutable"), gettext_noop("Default"), gettext_noop("Transactional end action")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 3fb2c4e9248..b653dda1fe8 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1273,6 +1273,8 @@ static const pgsql_thing_t words_after_create[] = { {"FOREIGN TABLE", NULL, NULL, NULL}, {"FUNCTION", NULL, NULL, Query_for_list_of_functions}, {"GROUP", Query_for_list_of_roles}, + {"IMMUTABLE VARIABLE", NULL, NULL, NULL, NULL, THING_NO_DROP | THING_NO_ALTER}, /* for CREATE IMMUTABLE + * VARIABLE ... */ {"INDEX", NULL, NULL, &Query_for_list_of_indexes}, {"LANGUAGE", Query_for_list_of_languages}, {"LARGE OBJECT", NULL, NULL, NULL, NULL, THING_NO_CREATE | THING_NO_DROP}, @@ -3593,7 +3595,8 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); + COMPLETE_WITH("IMMUTABLE VARIABLE", "SEQUENCE", "TABLE", "VARIABLE", + "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3945,7 +3948,8 @@ match_previous_words(int pattern_id, /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ else if (TailMatches("CREATE", "VARIABLE", MatchAny) || - TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny) || + TailMatches("IMMUTABLE", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ @@ -4589,6 +4593,10 @@ match_previous_words(int pattern_id, else if (TailMatches("FROM", "SERVER", MatchAny, "INTO", MatchAny)) COMPLETE_WITH("OPTIONS ("); +/* IMMUTABLE -- can be expend to IMMUTABLE VARIABLE */ + else if (TailMatches("CREATE", "IMMUTABLE")) + COMPLETE_WITH("VARIABLE"); + /* INSERT --- can be inside EXPLAIN, RULE, etc */ /* Complete NOT MATCHED THEN INSERT */ else if (TailMatches("NOT", "MATCHED", "THEN", "INSERT")) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index e8bfeebed11..68bc49a0e27 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,12 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* don't allow NULL */ + bool varnotnull BKI_DEFAULT(f); + + /* don't allow changes */ + bool varisimmutable BKI_DEFAULT(f); + /* action on transaction end */ char varxactendaction BKI_DEFAULT(n); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 60404413c49..7308c323574 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,8 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + bool not_null; /* disallow nulls */ + bool is_immutable; /* don't allow changes */ Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index bc7b60a97ea..77a7bf45c8a 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | t | t | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action ---------+------+------+-----------+-------+---------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action +--------+------+------+-----------+-------+----------+---------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index ee153f8fb82..a8520e5579d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Nullable | Mutable | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+----------+---------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | t | t | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1617,3 +1617,148 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The session variable was not initialized yet. +--should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1; +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; +--should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +DISCARD VARIABLES; +-- should to fail +LET var1 = NULL; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DROP VARIABLE var1; +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); +-- should to fail +SELECT var1; +ERROR: null value is not allowed for NOT NULL session variable "public.var1" +DETAIL: The result of DEFAULT expression is NULL. +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +SELECT var1; + var1 +------ + 10 +(1 row) + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 0 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + +(1 row) + +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DISCARD VARIABLES; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should fail +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; +-- should to fail +LET var1 = 10; +ERROR: session variable "public.var1" is declared IMMUTABLE +LET var1 = 20; +ERROR: session variable "public.var1" is declared IMMUTABLE +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- should to fail +LET var1 = 30; +ERROR: session variable "public.var1" is declared IMMUTABLE +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 9d9d04ec76b..ac8e2cb0943 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1086,3 +1086,125 @@ SELECT var1; DROP VARIABLE var1; DROP FUNCTION vartest_fx(); + +-- test NOT NULL +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL; +-- should to fail +SELECT var1; + +--should be ok +LET var1 = 10; +SELECT var1; + +DROP VARIABLE var1; + +-- should be ok +CREATE VARIABLE var1 AS int NOT NULL DEFAULT 0; + +--should be ok +SELECT var1; + +-- should be ok +LET var1 = 10; +SELECT var1; + +DISCARD VARIABLES; + +-- should to fail +LET var1 = NULL; + +DROP VARIABLE var1; + +-- test NOT NULL +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int NOT NULL DEFAULT vartest_fx(); + +-- should to fail +SELECT var1; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +SELECT var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); + +-- test IMMUTBLE +CREATE IMMUTABLE VARIABLE var1 AS int; + +-- should be ok +SELECT var1; +-- first write should ok +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +LET var1 = 10; +-- should fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should be ok +LET var1 = NULL; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +CREATE IMMUTABLE VARIABLE var1 AS int DEFAULT 10; + +-- don't allow change when variable has DEFAULT value +-- should to fail +LET var1 = 20; + +DISCARD VARIABLES; + +-- should be ok +SELECT var1; +-- should fail +LET var1 = 20; + +DROP VARIABLE var1; + +-- should be ok +CREATE IMMUTABLE VARIABLE var1 AS INT NOT NULL DEFAULT 10; + +-- should to fail +LET var1 = 10; +LET var1 = 20; + +-- should be ok +SELECT var1; + +-- should to fail +LET var1 = 30; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] v20241120-0012-Implementation-of-DEFAULT-clause-default-expressions.patch (33.6K, 12-v20241120-0012-Implementation-of-DEFAULT-clause-default-expressions.patch) download | inline diff: From af34b0c5aa56d85dab0277d791ed3da371dddcae Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:03:53 +0100 Subject: [PATCH 12/21] Implementation of DEFAULT clause - default expressions for session variables When the evaluation of default expression fails, we remove related entry from sessionvars hash table. Then sessionvars can contain only sucessfully initialized values. Then we don't need special flag for badly initialized session variables. --- doc/src/sgml/catalogs.sgml | 8 ++ doc/src/sgml/ddl.sgml | 16 ++-- doc/src/sgml/ref/create_variable.sgml | 21 +++-- doc/src/sgml/ref/discard.sgml | 3 +- src/backend/catalog/pg_variable.c | 27 ++++++ src/backend/commands/session_variable.c | 87 ++++++++++++++++++- src/backend/parser/gram.y | 15 +++- src/backend/parser/parse_agg.c | 2 + src/backend/parser/parse_expr.c | 4 + src/backend/parser/parse_func.c | 1 + src/bin/pg_dump/pg_dump.c | 14 +++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++++++++ src/bin/psql/describe.c | 4 +- src/include/catalog/pg_variable.h | 3 + src/include/nodes/parsenodes.h | 1 + src/include/parser/parse_node.h | 1 + src/test/regress/expected/psql.out | 36 ++++---- .../regress/expected/session_variables.out | 73 ++++++++++++++-- src/test/regress/sql/session_variables.sql | 48 ++++++++++ 20 files changed, 355 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 02b6b27c5cb..124902c4e44 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9874,6 +9874,14 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vardefexpr</structfield> <type>pg_node_tree</type> + </para> + <para> + The internal representation of the variable default value + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 049a31a6da2..600c153ee29 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5339,14 +5339,14 @@ SELECT current_user_id; <para> The value of a session variable is local to the current session. Retrieving - a variable's value returns a <literal>NULL</literal>, unless its value has - been set to something else in the current session using the - <command>LET</command> command. Session variables are not transactional: - any changes made to the value of a session variable in a transaction won't - be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves can be persistent - or temporary, but their values are neither persistent nor shared (like the - content of temporary tables). + a variable's value returns a <literal>NULL</literal> or a default value, + unless its value has been set to something else in the current session + using the <command>LET</command> command. Session variables are not + transactional: any changes made to the value of a session variable in + a transaction won't be undone if the transaction is rolled back (just like + variables in procedural languages). Session variables themselves can be + persistent or temporary, but their values are neither persistent nor shared + (like the content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index e1c2940d4ca..8187c18e28b 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] + [ DEFAULT <replaceable class="parameter">default_expr</replaceable> ] [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -41,10 +41,10 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p <para> The value of a session variable is local to the current session. Retrieving - a session variable's value returns NULL, unless its value is set to - something else in the current session with a <command>LET</command> command. - The content of a session variable is not transactional. This is the same as - regular variables in procedural languages. + a session variable's value returns NULL or a default value, unless its value + is set to something else in the current session with a <command>LET</command> + command. The content of a session variable is not transactional. This is the + same as regular variables in procedural languages. </para> <para> @@ -105,6 +105,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-default"> + <term><literal>DEFAULT <replaceable>default_expr</replaceable></literal></term> + <listitem> + <para> + The <literal>DEFAULT</literal> clause can be used to assign a default + value to a session variable. This expression is evaluated when the session + variable is first accessed for reading and had not yet been assigned a value. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> <term><literal>ON COMMIT DROP</literal></term> <listitem> diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index 61b967f9c9b..6b0cb95034f 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -71,7 +71,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } <listitem> <para> Resets the value of all session variables. If a variable - is later reused, it is re-initialized to <literal>NULL</literal>. + is later reused, it is re-initialized to either + <literal>NULL</literal> or its default value. </para> </listitem> </varlistentry> diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 4520eed6da8..36de12587c0 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -24,6 +24,9 @@ #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "miscadmin.h" +#include "parser/parse_coerce.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -37,6 +40,7 @@ static ObjectAddress create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction); @@ -51,6 +55,7 @@ create_variable(const char *varName, Oid varOwner, Oid varCollation, bool if_not_exists, + Node *varDefexpr, VariableXactEndAction varXactEndAction) { Acl *varacl; @@ -114,6 +119,11 @@ create_variable(const char *varName, values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); + if (varDefexpr) + values[Anum_pg_variable_vardefexpr - 1] = CStringGetTextDatum(nodeToString(varDefexpr)); + else + nulls[Anum_pg_variable_vardefexpr - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); if (varacl != NULL) @@ -150,6 +160,11 @@ create_variable(const char *varName, record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); free_object_addresses(addrs); + /* dependency on default expr */ + if (varDefexpr) + recordDependencyOnExpr(&myself, (Node *) varDefexpr, + NIL, DEPENDENCY_NORMAL); + /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); @@ -185,6 +200,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid collation; Oid typcollation; ObjectAddress variable; + Node *cooked_default = NULL; /* check consistency of arguments */ if (stmt->XactEndAction == VARIABLE_XACTEND_DROP @@ -226,6 +242,16 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) format_type_be(typid)), parser_errposition(pstate, stmt->collClause->location))); + if (stmt->defexpr) + { + cooked_default = transformExpr(pstate, stmt->defexpr, + EXPR_KIND_VARIABLE_DEFAULT); + + cooked_default = coerce_to_specific_type(pstate, + cooked_default, typid, "DEFAULT"); + assign_expr_collations(pstate, cooked_default); + } + variable = create_variable(stmt->variable->relname, namespaceid, typid, @@ -233,6 +259,7 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) varowner, collation, stmt->if_not_exists, + cooked_default, stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0b512815d4a..0a1c326e72e 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/svariableReceiver.h" #include "funcapi.h" #include "miscadmin.h" +#include "optimizer/optimizer.h" #include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" @@ -510,11 +511,68 @@ AtEOSubXact_SessionVariables(bool isCommit, } } +/* + * evaluate an expression + */ +static void +eval_assign_defexpr(SVariable svar, HeapTuple tup) +{ + Datum defexpr_value; + bool isnull; + + Assert(svar); + Assert(svar->is_valid); + Assert(HeapTupleIsValid(tup)); + + defexpr_value = SysCacheGetAttr(VARIABLEOID, + tup, + Anum_pg_variable_vardefexpr, + &isnull); + + if (!isnull) + { + EState *estate; + ExprState *defexprs; + Expr *defexpr; + char *defexpr_str; + Datum value; + MemoryContext oldcxt; + + estate = CreateExecutorState(); + + defexpr_str = TextDatumGetCString(defexpr_value); + defexpr = (Expr *) stringToNode(defexpr_str); + + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + + defexpr = expression_planner((Expr *) defexpr); + defexprs = ExecInitExpr(defexpr, NULL); + + value = ExecEvalExprSwitchContext(defexprs, + GetPerTupleExprContext(estate), + &isnull); + + MemoryContextSwitchTo(oldcxt); + + if (!isnull) + { + oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + svar->value = datumCopy(value, svar->typbyval, svar->typlen); + svar->isnull = false; + + MemoryContextSwitchTo(oldcxt); + } + + FreeExecutorState(estate); + } +} + /* * Initialize attributes cached in "svar" */ static void -setup_session_variable(SVariable svar, Oid varid) +setup_session_variable(SVariable svar, Oid varid, bool is_write) { HeapTuple tup; Form_pg_variable varform; @@ -564,6 +622,9 @@ setup_session_variable(SVariable svar, Oid varid) svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!is_write) + eval_assign_defexpr(svar, tup); + ReleaseSysCache(tup); } @@ -593,7 +654,7 @@ set_session_variable(SVariable svar, Datum value, bool isnull) */ if (!svar->is_valid) { - setup_session_variable(&locsvar, svar->varid); + setup_session_variable(&locsvar, svar->varid, false); _svar = &locsvar; } else @@ -726,7 +787,25 @@ get_session_variable(Oid varid) */ if (!svar->is_valid) { - setup_session_variable(svar, varid); + /* in this case we want to use defexp if it is defined */ + PG_TRY(); + { + /* + * In this case, the setup can execute default expression. When + * the execution of default expression fails, then we need to + * remove entry from session vars. + */ + setup_session_variable(svar, varid, false); + } + PG_CATCH(); + { + /* this entry cannot be valid, remove from sessionvars */ + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + + /* propagate the error */ + PG_RE_THROW(); + } + PG_END_TRY(); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", get_namespace_name(get_session_variable_namespace(varid)), @@ -790,7 +869,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!found) { - setup_session_variable(svar, varid); + setup_session_variable(svar, varid, true); elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", get_namespace_name(get_session_variable_namespace(svar->varid)), diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d8de8acf944..b8e33ed19db 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -638,6 +638,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <partboundspec> PartitionBoundSpec %type <list> hash_partbound %type <defelt> hash_partbound_elem +%type <node> OptSessionVarDefExpr %type <node> json_format_clause json_format_clause_opt @@ -5230,30 +5231,36 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $4->relpersistence = $2; n->variable = $4; n->typeName = $6; n->collClause = (CollateClause *) $7; - n->XactEndAction = $8; + n->defexpr = $8; + n->XactEndAction = $9; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause OptSessionVarDefExpr XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); $7->relpersistence = $2; n->variable = $7; n->typeName = $9; n->collClause = (CollateClause *) $10; - n->XactEndAction = $11; + n->defexpr = $11; + n->XactEndAction = $12; n->if_not_exists = true; $$ = (Node *) n; } ; +OptSessionVarDefExpr: DEFAULT b_expr { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + /* * Temporary session variables can be dropped on successful * transaction end like tables. diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index a7343001f21..ddfbcf42b74 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -488,6 +488,7 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: if (isAgg) err = _("aggregate functions are not allowed in DEFAULT expressions"); @@ -937,6 +938,7 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("window functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 3602c3572d7..bcbb1f564af 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -582,6 +582,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind) case EXPR_KIND_JOIN_USING: case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: result = false; break; } @@ -667,6 +668,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_CYCLE_MARK: case EXPR_KIND_ASSIGN_TARGET: case EXPR_KIND_LET_TARGET: + case EXPR_KIND_VARIABLE_DEFAULT: /* okay */ break; @@ -2075,6 +2077,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("cannot use subquery in DEFAULT expression"); break; case EXPR_KIND_INDEX_EXPRESSION: @@ -3427,6 +3430,7 @@ ParseExprKindName(ParseExprKind exprKind) return "CHECK"; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: return "DEFAULT"; case EXPR_KIND_INDEX_EXPRESSION: return "index expression"; diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9aa4f60768b..fcac694455d 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2620,6 +2620,7 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) break; case EXPR_KIND_COLUMN_DEFAULT: case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_VARIABLE_DEFAULT: err = _("set-returning functions are not allowed in DEFAULT expressions"); break; case EXPR_KIND_INDEX_EXPRESSION: diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 2ec6a22a266..c138994304a 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_vardefexpr; int i_varxactendaction; int i_varowner; int i_varcollation; @@ -5458,6 +5459,7 @@ getVariables(Archive *fout) " THEN v.varcollation\n" " ELSE 0\n" " END AS varcollation,\n" + " pg_catalog.pg_get_expr(v.vardefexpr,0) as vardefexpr,\n" " v.varowner, v.varacl,\n" " acldefault('V', v.varowner) AS acldefault\n" "FROM pg_catalog.pg_variable v\n" @@ -5474,6 +5476,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_vardefexpr = PQfnumber(res, "vardefexpr"); i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); @@ -5509,6 +5512,11 @@ getVariables(Archive *fout) varinfo[i].dacl.initprivs = NULL; varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + if (PQgetisnull(res, i, i_vardefexpr)) + varinfo[i].vardefexpr = NULL; + else + varinfo[i].vardefexpr = pg_strdup(PQgetvalue(res, i, i_vardefexpr)); + /* do not try to dump ACL if no ACL exists */ if (!PQgetisnull(res, i, i_varacl)) varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; @@ -5541,6 +5549,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *vardefexpr; const char *varxactendaction; Oid varcollation; @@ -5553,6 +5562,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + vardefexpr = varinfo->vardefexpr; varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; @@ -5572,6 +5582,10 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (vardefexpr) + appendPQExpBuffer(query, " DEFAULT %s", + vardefexpr); + if (strcmp(varxactendaction, "r") == 0) appendPQExpBuffer(query, " ON TRANSACTION END RESET"); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7c0c03749e8..8ed83979ec9 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *vardefexpr; char *varxactendaction; char *varacl; char *rvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 419ea39727b..f58ede5334a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -4005,6 +4005,42 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable DEFAULT' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable3 AS integer DEFAULT 10;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CREATE VARIABLE test_variable DEFAULT ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable4 AS integer DEFAULT 10 ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d0f69f8c8e9..dc18da350eb 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5227,6 +5227,7 @@ listVariables(const char *pattern, bool verbose) " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " pg_catalog.pg_get_expr(v.vardefexpr, 0) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" @@ -5236,6 +5237,7 @@ listVariables(const char *pattern, bool verbose) gettext_noop("Type"), gettext_noop("Collation"), gettext_noop("Owner"), + gettext_noop("Default"), gettext_noop("Transactional end action")); if (verbose) diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 60291ef7ac2..e8bfeebed11 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -67,6 +67,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* access permissions */ aclitem varacl[1] BKI_DEFAULT(_null_); + /* list of expression trees for variable default (NULL if none) */ + pg_node_tree vardefexpr BKI_DEFAULT(_null_); + #endif } FormData_pg_variable; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 7a519e08429..60404413c49 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + Node *defexpr; /* default expression */ char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index c11fdb3d970..fa566b194d1 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -84,6 +84,7 @@ typedef enum ParseExprKind EXPR_KIND_CYCLE_MARK, /* cycle mark value */ EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ EXPR_KIND_LET_TARGET, /* LET target */ + EXPR_KIND_VARIABLE_DEFAULT, /* default value for session variable */ } ParseExprKind; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 61077a52b16..bc7b60a97ea 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action ---------+------+------+-----------+-------+-------------------------- + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action +--------+------+------+-----------+-------+---------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 99e4ac72be1..ee153f8fb82 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| - | | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Default | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+---------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1554,3 +1554,66 @@ SELECT var1 IS NULL; (1 row) DROP VARIABLE var1; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); +ERROR: cannot drop function vartest_fx() because other objects depend on it +DETAIL: session variable var1 depends on function vartest_fx() +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +SELECT var1; +NOTICE: vartest_fx executed + var1 +------ + 0 +(1 row) + +-- the defexpr should be evaluated only once +SELECT var1; + var1 +------ + 0 +(1 row) + +DISCARD VARIABLES; +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DISCARD VARIABLES; +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; +-- should to fail, but not to crash +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- again +SELECT var1; +ERROR: vartest_fx is executing +CONTEXT: PL/pgSQL function vartest_fx() line 3 at RAISE +-- but we can write +LET var1 = 100; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 7853261080b..9d9d04ec76b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1038,3 +1038,51 @@ ROLLBACK; SELECT var1 IS NULL; DROP VARIABLE var1; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE NOTICE 'vartest_fx executed'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +CREATE VARIABLE var1 AS int DEFAULT vartest_fx(); + +-- vartest_fx should be protected by dep, should fail +DROP FUNCTION vartest_fx(); + +-- should be ok +SELECT var1; + +-- the defexpr should be evaluated only once +SELECT var1; + +DISCARD VARIABLES; + +-- in this case, the defexpr should not be evaluated +LET var1 = 100; +SELECT var1; + +DISCARD VARIABLES; + +CREATE OR REPLACE FUNCTION vartest_fx() +RETURNS int AS $$ +BEGIN + RAISE EXCEPTION 'vartest_fx is executing'; + RETURN 0; +END; +$$ LANGUAGE plpgsql; + +-- should to fail, but not to crash +SELECT var1; + +-- again +SELECT var1; + +-- but we can write +LET var1 = 100; +SELECT var1; + +DROP VARIABLE var1; +DROP FUNCTION vartest_fx(); -- 2.47.0 [text/x-patch] v20241120-0010-implementation-of-temporary-session-variables.patch (42.2K, 13-v20241120-0010-implementation-of-temporary-session-variables.patch) download | inline diff: From 5708d8206c9eaa69928dc638ebe229c22638ac68 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:00:28 +0100 Subject: [PATCH 10/21] implementation of temporary session variables The temporary variables are created inside temp schema and they are dropped by dropping this schema. For consistency with temp tables, the CREATE TEMP VARIABLE command supports ON COMMIT DROP clause. The temporary variables with this clause are collected in xact_drop_items list. This list should be carefully maintained and can contain only valid entries (not yet dropped). From this reasons now the subcommit and subaborting have to be handled. --- doc/src/sgml/catalogs.sgml | 10 + doc/src/sgml/ddl.sgml | 6 +- doc/src/sgml/ref/create_variable.sgml | 15 +- src/backend/access/transam/xact.c | 9 + src/backend/catalog/pg_variable.c | 24 +- src/backend/commands/session_variable.c | 218 +++++++++++++++++- src/backend/commands/view.c | 2 +- src/backend/parser/analyze.c | 4 +- src/backend/parser/gram.y | 30 ++- src/backend/parser/parse_relation.c | 22 +- src/bin/psql/describe.c | 10 +- src/bin/psql/tab-complete.in.c | 5 +- src/include/catalog/pg_variable.h | 9 + src/include/commands/session_variable.h | 6 +- src/include/nodes/parsenodes.h | 1 + src/include/nodes/primnodes.h | 4 +- src/include/parser/parse_relation.h | 2 +- .../isolation/expected/session-variable.out | 3 +- .../isolation/specs/session-variable.spec | 3 +- src/test/regress/expected/psql.out | 36 +-- .../regress/expected/session_variables.out | 130 ++++++++++- src/test/regress/sql/session_variables.sql | 66 ++++++ src/tools/pgindent/typedefs.list | 1 + 23 files changed, 546 insertions(+), 70 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 0c15d56dd42..94db502b72e 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9840,6 +9840,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varxactendaction</structfield> <type>char</type> + </para> + <para> + Action performed at end of transaction: + <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varcollation</structfield> <type>oid</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index ce1b1e1f599..049a31a6da2 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5344,9 +5344,9 @@ SELECT current_user_id; <command>LET</command> command. Session variables are not transactional: any changes made to the value of a session variable in a transaction won't be undone if the transaction is rolled back (just like variables in - procedural languages). Session variables themselves are persistent, but - their values are neither persistent nor shared (like the content of - temporary tables). + procedural languages). Session variables themselves can be persistent + or temporary, but their values are neither persistent nor shared (like the + content of temporary tables). </para> <para> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 05bf67e3411..642346186f0 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -26,7 +26,8 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] + [ ON COMMIT DROP ] </synopsis> </refsynopsisdiv> <refsect1> @@ -104,6 +105,18 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-commit-drop"> + <term><literal>ON COMMIT DROP</literal></term> + <listitem> + <para> + The <literal>ON COMMIT DROP</literal> clause specifies the behaviour of a + temporary session variable at transaction commit. With this clause, the + session variable is dropped at commit time. The clause is only allowed + for temporary variables. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b7ebcc2a557..01e3d469306 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -36,6 +36,7 @@ #include "catalog/pg_enum.h" #include "catalog/storage.h" #include "commands/async.h" +#include "commands/session_variable.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "common/pg_prng.h" @@ -2316,6 +2317,9 @@ CommitTransaction(void) */ smgrDoPendingSyncs(true, is_parallel_worker); + /* remove values of dropped session variables from memory */ + AtPreEOXact_SessionVariables(true); + /* close large objects before lower-level cleanup */ AtEOXact_LargeObject(true); @@ -2912,6 +2916,7 @@ AbortTransaction(void) AtAbort_Portals(); smgrDoPendingSyncs(false, is_parallel_worker); AtEOXact_LargeObject(false); + AtPreEOXact_SessionVariables(false); AtAbort_Notify(); AtEOXact_RelationMap(false, is_parallel_worker); AtAbort_Twophase(); @@ -5190,6 +5195,8 @@ CommitSubTransaction(void) AtEOSubXact_SPI(true, s->subTransactionId); AtEOSubXact_on_commit_actions(true, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(true, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(true, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(true, s->subTransactionId, @@ -5355,6 +5362,8 @@ AbortSubTransaction(void) AtEOSubXact_SPI(false, s->subTransactionId); AtEOSubXact_on_commit_actions(false, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(false, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(false, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(false, s->subTransactionId, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index c7369e2b2ac..4520eed6da8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -36,7 +36,8 @@ static ObjectAddress create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists); + bool if_not_exists, + VariableXactEndAction varXactEndAction); /* @@ -49,7 +50,8 @@ create_variable(const char *varName, int32 varTypmod, Oid varOwner, Oid varCollation, - bool if_not_exists) + bool if_not_exists, + VariableXactEndAction varXactEndAction) { Acl *varacl; NameData varname; @@ -110,6 +112,7 @@ create_variable(const char *varName, values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + values[Anum_pg_variable_varxactendaction - 1] = CharGetDatum(varXactEndAction); varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, varNamespace); @@ -183,6 +186,13 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) Oid typcollation; ObjectAddress variable; + /* check consistency of arguments */ + if (stmt->XactEndAction == VARIABLE_XACTEND_DROP + && stmt->variable->relpersistence != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("ON COMMIT DROP can only be used on temporary variables"))); + namespaceid = RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); @@ -222,7 +232,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) typmod, varowner, collation, - stmt->if_not_exists); + stmt->if_not_exists, + stmt->XactEndAction); elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", stmt->variable->relname, variable.objectId); @@ -230,6 +241,8 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) /* we want SessionVariableCreatePostprocess to see the catalog changes. */ CommandCounterIncrement(); + SessionVariableCreatePostprocess(variable.objectId, stmt->XactEndAction); + return variable; } @@ -242,6 +255,7 @@ DropVariableById(Oid varid) { Relation rel; HeapTuple tup; + char XactEndAction; rel = table_open(VariableRelationId, RowExclusiveLock); @@ -250,6 +264,8 @@ DropVariableById(Oid varid) if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for variable %u", varid); + XactEndAction = ((Form_pg_variable) GETSTRUCT(tup))->varxactendaction; + CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); @@ -257,5 +273,5 @@ DropVariableById(Oid varid) table_close(rel, RowExclusiveLock); /* do the necessary cleanup in local memory, if needed */ - SessionVariableDropPostprocess(varid); + SessionVariableDropPostprocess(varid, XactEndAction); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 21c7a9517b5..b23ae21ba55 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -16,6 +16,8 @@ #include "access/xact.h" #include "catalog/pg_variable.h" +#include "catalog/dependency.h" +#include "catalog/namespace.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" #include "funcapi.h" @@ -31,6 +33,19 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" +typedef struct SVariableXActDropItem +{ + Oid varid; /* varid of session variable */ + + /* + * creating_subid is the ID of the creating subxact. If the action was + * unregistered during the current transaction, deleting_subid is the ID + * of the deleting subxact, otherwise InvalidSubTransactionId. + */ + SubTransactionId creating_subid; + SubTransactionId deleting_subid; +} SVariableXActDropItem; + /* * The values of session variables are stored in the backend's private memory * in the dedicated memory context SVariableMemoryContext in binary format. @@ -96,13 +111,21 @@ static MemoryContext SVariableMemoryContext = NULL; static bool needs_validation = false; /* - * The content of dropped session variables is not removed immediately. We do - * that in the next transaction that reads or writes a session variable. - * "validated_lxid" stores the transaction that performed said validation, so - * that we can avoid repeating the effort. + * The content of dropped session variables is not removed immediately. If + * possible, we do that at the end of the transaction. But we cannot do that + * if the transaction aborted, because we lost access to the system catalog. + * In that case, we clean up in the next transaction that reads or writes a + * session variable. "validated_lxid" stores the transaction that performed + * said validation, so that we can avoid repeating the effort. */ static LocalTransactionId validated_lxid = InvalidLocalTransactionId; +/* list holds fields of SVariableXActDropItem type */ +static List *xact_drop_items = NIL; + +static void register_session_variable_xact_drop(Oid varid); +static void unregister_session_variable_xact_drop(Oid varid); + /* * Callback function for session variable invalidation. */ @@ -139,16 +162,45 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) } } +/* + * Do the necessary work to setup local memory management of a new + * variable. + * + * Caller should already have created the necessary entry in catalog + * and made them visible. + */ +void +SessionVariableCreatePostprocess(Oid varid, char XactEndAction) +{ + /* + * For temporary variables, we need to create a new end of xact action to + * ensure deletion from catalog. + */ + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + register_session_variable_xact_drop(varid); + } +} + /* * Handle the local memory cleanup for a DROP VARIABLE command. * * Caller should take care of removing the pg_variable entry first. */ void -SessionVariableDropPostprocess(Oid varid) +SessionVariableDropPostprocess(Oid varid, char XactEndAction) { Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + if (XactEndAction == VARIABLE_XACTEND_DROP) + { + Assert(isTempNamespace(get_session_variable_namespace(varid))); + + unregister_session_variable_xact_drop(varid); + } + if (sessionvars) { bool found; @@ -170,6 +222,57 @@ SessionVariableDropPostprocess(Oid varid) } } +/* + * Registration of actions to be executed on session variables at transaction + * end time. We want to drop temporary session variables with clause ON COMMIT + * DROP. + */ + +/* + * Register a session variable xact action. + */ +static void +register_session_variable_xact_drop(Oid varid) +{ + SVariableXActDropItem *xact_ai; + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(CacheMemoryContext); + + xact_ai = (SVariableXActDropItem *) + palloc(sizeof(SVariableXActDropItem)); + + xact_ai->varid = varid; + + xact_ai->creating_subid = GetCurrentSubTransactionId(); + xact_ai->deleting_subid = InvalidSubTransactionId; + + xact_drop_items = lcons(xact_ai, xact_drop_items); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Unregister an id of a given session variable from drop list. In this + * moment, the action is just marked as deleted by setting deleting_subid. The + * calling even might be rollbacked, in which case we should not lose this + * action. + */ +static void +unregister_session_variable_xact_drop(Oid varid) +{ + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + if (xact_ai->varid == varid) + xact_ai->deleting_subid = GetCurrentSubTransactionId(); + } +} + /* * Release stored value, free memory */ @@ -222,9 +325,11 @@ is_session_variable_valid(SVariable svar) /* * Check all potentially invalid session variable data in local memory and free * the memory for all invalid ones. This function is called before any read or - * write of a session variable. Freeing of a variable's memory is postponed if - * the variable has been dropped by the current transaction, since that - * operation could still be rolled back. + * write of a session variable or when the transaction ends. At the end of a + * transaction (atEOX is true) we can discard all invalid variables. Inside a + * transaction (atEOX is false) we postpone freeing a variable's memory if the + * variable has been dropped by the current transaction, since that operation + * could still be rolled back. * * It is possible that we receive a cache invalidation message while * remove_invalid_session_variables() is executing, so we cannot guarantee that @@ -232,7 +337,7 @@ is_session_variable_valid(SVariable svar) * done. However, we can guarantee that all entries get checked once. */ static void -remove_invalid_session_variables(void) +remove_invalid_session_variables(bool atEOX) { HASH_SEQ_STATUS status; SVariable svar; @@ -259,7 +364,7 @@ remove_invalid_session_variables(void) { if (!svar->is_valid) { - if (svar->drop_lxid == MyProc->vxid.lxid) + if (!atEOX && svar->drop_lxid == MyProc->vxid.lxid) { /* try again in the next transaction */ needs_validation = true; @@ -280,6 +385,95 @@ remove_invalid_session_variables(void) } } +/* + * Perform ON COMMIT DROP for temporary session variables, + * and remove all dropped variables from memory. + */ +void +AtPreEOXact_SessionVariables(bool isCommit) +{ + if (isCommit) + { + if (xact_drop_items) + { + ListCell *l; + + foreach(l, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(l); + + /* iterate only over entries that are still pending */ + if (xact_ai->deleting_subid == InvalidSubTransactionId) + { + ObjectAddress object; + + object.classId = VariableRelationId; + object.objectId = xact_ai->varid; + object.objectSubId = 0; + + /* + * Since this is an automatic drop, rather than one + * directly initiated by the user, we pass the + * PERFORM_DELETION_INTERNAL flag. + */ + elog(DEBUG1, "session variable (oid:%u) will be deleted (forced by ON COMMIT DROP clause)", + xact_ai->varid); + + performDeletion(&object, DROP_CASCADE, + PERFORM_DELETION_INTERNAL | + PERFORM_DELETION_QUIETLY); + } + } + } + + remove_invalid_session_variables(true); + } + + /* + * We have to clean xact_drop_items. All related variables are dropped + * now, or lost inside aborted transaction. + */ + list_free_deep(xact_drop_items); + xact_drop_items = NULL; +} + +/* + * Post-subcommit or post-subabort cleanup of xact drop list. + * + * During subabort, we can immediately remove entries created during this + * subtransaction. During subcommit, just transfer entries marked during + * this subtransaction as being the parent's responsibility. + */ +void +AtEOSubXact_SessionVariables(bool isCommit, + SubTransactionId mySubid, + SubTransactionId parentSubid) +{ + ListCell *cur_item; + + foreach(cur_item, xact_drop_items) + { + SVariableXActDropItem *xact_ai = + (SVariableXActDropItem *) lfirst(cur_item); + + if (!isCommit && xact_ai->creating_subid == mySubid) + { + /* cur_item must be removed */ + xact_drop_items = foreach_delete_current(xact_drop_items, cur_item); + pfree(xact_ai); + } + else + { + /* cur_item must be preserved */ + if (xact_ai->creating_subid == mySubid) + xact_ai->creating_subid = parentSubid; + if (xact_ai->deleting_subid == mySubid) + xact_ai->deleting_subid = isCommit ? parentSubid : InvalidSubTransactionId; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -438,7 +632,7 @@ get_session_variable(Oid varid) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; @@ -534,7 +728,7 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) validated_lxid != MyProc->vxid.lxid) { /* free the memory from dropped session variables */ - remove_invalid_session_variables(); + remove_invalid_session_variables(false); /* don't repeat the above step in the same transaction */ validated_lxid = MyProc->vxid.lxid; diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 2bd49eb55e6..c736c5c46f3 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -484,7 +484,7 @@ DefineView(ViewStmt *stmt, const char *queryString, */ view = copyObject(stmt->view); /* don't corrupt original command */ if (view->relpersistence == RELPERSISTENCE_PERMANENT - && isQueryUsingTempRelation(viewParse)) + && isQueryUsingTempObject(viewParse)) { view->relpersistence = RELPERSISTENCE_TEMP; ereport(NOTICE, diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 81b8b0a633d..9dfd435128a 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -3390,10 +3390,10 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) * creation query. It would be hard to refresh data or incrementally * maintain it if a source disappeared. */ - if (isQueryUsingTempRelation(query)) + if (isQueryUsingTempObject(query)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("materialized views must not use temporary tables or views"))); + errmsg("materialized views must not use temporary tables, views or session variables"))); /* * A materialized view would either need to save parameters for use in diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index af511d12aa1..76655aa8aeb 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -466,6 +466,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <ival> OptTemp %type <ival> OptNoLog %type <oncommit> OnCommitOption +%type <ival> XactEndActionOption %type <ival> for_locking_strength %type <node> for_locking_item @@ -5229,26 +5230,39 @@ create_extension_opt_item: *****************************************************************************/ CreateSessionVarStmt: - CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + CREATE OptTemp VARIABLE qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $3; - n->typeName = $5; - n->collClause = (CollateClause *) $6; + $4->relpersistence = $2; + n->variable = $4; + n->typeName = $6; + n->collClause = (CollateClause *) $7; + n->XactEndAction = $8; n->if_not_exists = false; $$ = (Node *) n; } - | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause XactEndActionOption { CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); - n->variable = $6; - n->typeName = $8; - n->collClause = (CollateClause *) $9; + $7->relpersistence = $2; + n->variable = $7; + n->typeName = $9; + n->collClause = (CollateClause *) $10; + n->XactEndAction = $11; n->if_not_exists = true; $$ = (Node *) n; } ; +/* + * Temporary session variables can be dropped on successful + * transaction end like tables. + */ +XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } + ; + + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 8075b1b8a1b..95ae2108044 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -101,7 +101,7 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); -static bool isQueryUsingTempRelation_walker(Node *node, void *context); +static bool isQueryUsingTempObject_walker(Node *node, void *context); /* @@ -3896,13 +3896,13 @@ rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte) * the query is a temporary relation (table, view, or materialized view). */ bool -isQueryUsingTempRelation(Query *query) +isQueryUsingTempObject(Query *query) { - return isQueryUsingTempRelation_walker((Node *) query, NULL); + return isQueryUsingTempObject_walker((Node *) query, NULL); } static bool -isQueryUsingTempRelation_walker(Node *node, void *context) +isQueryUsingTempObject_walker(Node *node, void *context) { if (node == NULL) return false; @@ -3928,13 +3928,23 @@ isQueryUsingTempRelation_walker(Node *node, void *context) } return query_tree_walker(query, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context, QTW_IGNORE_JOINALIASES); } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (isAnyTempNamespace(get_session_variable_namespace(p->paramvarid))) + return true; + } + } return expression_tree_walker(node, - isQueryUsingTempRelation_walker, + isQueryUsingTempObject_walker, context); } diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index c52c8fd147f..d02f6416f0e 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5206,7 +5206,7 @@ listVariables(const char *pattern, bool verbose) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false}; if (pset.sversion < 180000) { @@ -5226,12 +5226,16 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" - " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" + " CASE v.varxactendaction\n" + " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), gettext_noop("Type"), gettext_noop("Collation"), - gettext_noop("Owner")); + gettext_noop("Owner"), + gettext_noop("Transactional end action")); if (verbose) { diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index df3e2539270..3fb2c4e9248 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -3593,7 +3593,7 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW"); + COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -3944,7 +3944,8 @@ match_previous_words(int pattern_id, } /* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VARIABLE <name> with AS */ - else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + else if (TailMatches("CREATE", "VARIABLE", MatchAny) || + TailMatches("TEMP|TEMPORARY", "VARIABLE", MatchAny)) COMPLETE_WITH("AS"); else if (TailMatches("VARIABLE", MatchAny, "AS")) /* Complete CREATE VARIABLE <name> with AS types */ diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index db593cd5bed..1e6dd98d415 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -55,6 +55,9 @@ CATALOG(pg_variable,9222,VariableRelationId) /* typmod for variable's type */ int32 vartypmod BKI_DEFAULT(-1); + /* action on transaction end */ + char varxactendaction BKI_DEFAULT(n); + /* variable collation */ Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); @@ -67,6 +70,12 @@ CATALOG(pg_variable,9222,VariableRelationId) #endif } FormData_pg_variable; +typedef enum VariableXactEndAction +{ + VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ + VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ +} VariableXactEndAction; + /* ---------------- * Form_pg_variable corresponds to a pointer to a tuple with * the format of the pg_variable relation. diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 3dab8ae2a48..dc83296b1ba 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,7 +21,11 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" -extern void SessionVariableDropPostprocess(Oid varid); +extern void SessionVariableCreatePostprocess(Oid varid, char XactEndAction); +extern void SessionVariableDropPostprocess(Oid varid, char XactEndAction); +extern void AtPreEOXact_SessionVariables(bool isCommit); +extern void AtEOSubXact_SessionVariables(bool isCommit, SubTransactionId mySubid, + SubTransactionId parentSubid); extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 04ab22bd492..7a519e08429 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3477,6 +3477,7 @@ typedef struct CreateSessionVarStmt TypeName *typeName; /* the type of variable */ CollateClause *collClause; bool if_not_exists; /* do nothing if it already exists */ + char XactEndAction; /* on transaction end action */ } CreateSessionVarStmt; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 4b2424d6d34..6174a5562c5 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -51,7 +51,9 @@ typedef struct Alias List *colnames; /* optional list of column aliases */ } Alias; -/* What to do at commit time for temporary relations */ +/* + * What to do at commit time for temporary relations or session variables. + */ typedef enum OnCommitAction { ONCOMMIT_NOOP, /* No ON COMMIT clause (do nothing) */ diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index 91fd8e243b5..a8117bcae3f 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -126,6 +126,6 @@ extern int attnameAttNum(Relation rd, const char *attname, bool sysColOK); extern const NameData *attnumAttName(Relation rd, int attid); extern Oid attnumTypeId(Relation rd, int attid); extern Oid attnumCollationId(Relation rd, int attid); -extern bool isQueryUsingTempRelation(Query *query); +extern bool isQueryUsingTempObject(Query *query); #endif /* PARSE_RELATION_H */ diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index 0a5579dc7ce..a609797dc5d 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,10 +86,11 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; +step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index c864fee4006..45e65d4085d 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -24,6 +24,7 @@ step create { CREATE VARIABLE myvar AS text; } session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } +step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } @@ -47,4 +48,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 88e21194711..61077a52b16 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5935,20 +5935,20 @@ CREATE ROLE regress_variable_owner; SET ROLE TO regress_variable_owner; CREATE VARIABLE var1 AS varchar COLLATE "C"; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+-------------------+------------- - public | var1 | character varying | C | regress_variable_owner | | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | | (1 row) GRANT SELECT ON VARIABLE var1 TO PUBLIC; COMMENT ON VARIABLE var1 IS 'some description'; \dV+ var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description ---------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ - public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description - | | | | | =r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | | =r/regress_variable_owner | (1 row) DROP VARIABLE var1; @@ -6416,9 +6416,9 @@ List of schemas (0 rows) \dV "no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted schema qualifications. @@ -6591,9 +6591,9 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" \dV "no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with current database and dotted schema qualifications. @@ -6730,9 +6730,9 @@ List of text search templates (0 rows) \dV regression."no.such.schema"."no.such.variable" - List of variables - Schema | Name | Type | Collation | Owner ---------+------+------+-----------+------- + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action +--------+------+------+-----------+-------+-------------------------- (0 rows) -- again, but with dotted database and dotted schema qualifications. diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index cc4b5964799..6dadb9074da 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -45,11 +45,11 @@ SET ROLE TO regress_variable_owner; CREATE VARIABLE svartest.var1 AS int; SET ROLE TO DEFAULT; \dV+ svartest.var1 - List of variables - Schema | Name | Type | Collation | Owner | Access privileges | Description -----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- - svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| - | | | | | regress_variable_reader=r/regress_variable_owner | + List of variables + Schema | Name | Type | Collation | Owner | Transactional end action | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | | regress_variable_owner=rw/regress_variable_owner+| + | | | | | | regress_variable_reader=r/regress_variable_owner | (1 row) DROP VARIABLE svartest.var1; @@ -1400,3 +1400,123 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; +NOTICE: view "var_test_view" will be a temporary view +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view var_test_view +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; + count +------- + 0 +(1 row) + +-- should be zero +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 65c2a79425d..770ce650685 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -826,6 +826,7 @@ BEGIN; DROP VARIABLE var1; SELECT var2; ROLLBACK; + -- should be ok SELECT var1; @@ -952,3 +953,68 @@ SELECT var1; DEALLOCATE p1; DROP VARIABLE var1; + +-- temporary variables +CREATE TEMP VARIABLE var1 AS int; +-- this view should be temporary +CREATE VIEW var_test_view AS SELECT var1; + +DROP VARIABLE var1 CASCADE; + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + DROP VARIABLE var1; +ROLLBACK; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); + +BEGIN; + CREATE TEMP VARIABLE var1 AS int ON COMMIT DROP; + LET var1 = 100; + SAVEPOINT s1; + DROP VARIABLE var1; + ROLLBACK TO s1; + SELECT var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_variable WHERE varname = 'var1'; +-- should be zero +SELECT count(*) FROM pg_session_variables(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 68e3c3c7b63..64ddf589b3c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2798,6 +2798,7 @@ SupportRequestWFuncMonotonic SVariable SVariableData SVariableState +SVariableXActDropItem Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] v20241120-0009-PREPARE-LET-support.patch (7.4K, 14-v20241120-0009-PREPARE-LET-support.patch) download | inline diff: From da1508c86e4e27d5a79072d706cebc4eb9cb376d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 09:44:58 +0100 Subject: [PATCH 09/21] 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.in.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 <replaceable class="parameter">name</replaceable> [ ( <replaceable class <listitem> <para> Any <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, - <command>DELETE</command>, <command>MERGE</command>, or <command>VALUES</command> - statement. + <command>DELETE</command>, <command>MERGE</command>, <command>VALUES</command>, + or <command>LET</command> statement. </para> </listitem> </varlistentry> diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index def7dde2330..af511d12aa1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12161,7 +12161,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.in.c b/src/bin/psql/tab-complete.in.c index 3ef4776d98a..df3e2539270 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4629,9 +4629,9 @@ match_previous_words(int pattern_id, 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 <variable> 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 a867fdd3e75..cc4b5964799 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -439,9 +439,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 @@ -1321,5 +1321,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 8a6dbffd54e..65c2a79425d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -893,5 +893,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.47.0 [text/x-patch] v20241120-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch (14.6K, 15-v20241120-0011-Implementation-ON-TRANSACTION-END-RESET-clause.patch) download | inline diff: From e4dfee742beb1576af051fee18ef3fbfe843af91 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 15:02:13 +0100 Subject: [PATCH 11/21] Implementation ON TRANSACTION END RESET clause This is simple patch - just add special flag to session variable memory entry. The entries with active this flag are removed from sessionvars hash table at transaction end. The "TRANSACTION END" is synonyms for "COMMIT ROLLBACK" but the "TRANSACTION END" is more illustrative and less confusing then "COMMIT ROLLBACK". --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ref/create_variable.sgml | 13 ++++- src/backend/commands/session_variable.c | 51 +++++++++++++++++++ src/backend/parser/gram.y | 1 + src/bin/pg_dump/pg_dump.c | 11 ++++ src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 18 +++++++ src/bin/psql/describe.c | 1 + src/include/catalog/pg_variable.h | 1 + .../isolation/expected/session-variable.out | 4 +- .../isolation/specs/session-variable.spec | 4 +- .../regress/expected/session_variables.out | 34 +++++++++++++ src/test/regress/sql/session_variables.sql | 20 ++++++++ 13 files changed, 158 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 94db502b72e..02b6b27c5cb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9846,7 +9846,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para> <para> Action performed at end of transaction: - <literal>n</literal> = no action, <literal>d</literal> = drop the variable. + <literal>n</literal> = no action, <literal>d</literal> = drop the variable, + <literal>r</literal> = reset the variable to its default value. </para></entry> </row> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 642346186f0..e1c2940d4ca 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -27,7 +27,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] - [ ON COMMIT DROP ] + [ { ON COMMIT DROP | ON TRANSACTION END RESET } ] </synopsis> </refsynopsisdiv> <refsect1> @@ -117,6 +117,17 @@ CREATE [ { TEMPORARY | TEMP } ] VARIABLE [ IF NOT EXISTS ] <replaceable class="p </listitem> </varlistentry> + <varlistentry id="sql-createvariable-on-transaction-end-reset"> + <term><literal>ON TRANSACTION END RESET</literal></term> + <listitem> + <para> + The <literal>ON TRANSACTION END RESET</literal> clause causes the session + variable to be reset to its default value when the transaction is committed + or rolled back. + </para> + </listitem> + </varlistentry> + </variablelist> </refsect1> diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index b23ae21ba55..0b512815d4a 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -83,6 +83,8 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + bool reset_at_eox; + /* * Top level local transaction id of the last transaction that dropped the * variable, if any. We need this information to avoid freeing memory for @@ -110,6 +112,12 @@ static MemoryContext SVariableMemoryContext = NULL; /* becomes true when we receive a sinval message */ static bool needs_validation = false; +/* + * true, when some used session variable has ON COMMIT DROP + * or ON TRANSACTION END RESET clauses + */ +static bool has_session_variables_with_reset_at_eox = false; + /* * The content of dropped session variables is not removed immediately. If * possible, we do that at the end of the transaction. But we cannot do that @@ -385,6 +393,32 @@ remove_invalid_session_variables(bool atEOX) } } +/* + * remove entries marked as "reset_at_eox" + */ +static void +remove_session_variables_with_reset_at_eox(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + if (!sessionvars) + return; + + /* leave quckly, when there are not that variables */ + if (!has_session_variables_with_reset_at_eox) + return; + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (svar->reset_at_eox) + hash_search(sessionvars, &svar->varid, HASH_REMOVE, NULL); + } + + has_session_variables_with_reset_at_eox = false; +} + /* * Perform ON COMMIT DROP for temporary session variables, * and remove all dropped variables from memory. @@ -392,6 +426,8 @@ remove_invalid_session_variables(bool atEOX) void AtPreEOXact_SessionVariables(bool isCommit) { + remove_session_variables_with_reset_at_eox(); + if (isCommit) { if (xact_drop_items) @@ -503,6 +539,21 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + /* + * We don't need to explicitly reset variables marked ON COMMIT DROP. It + * can be done by sinval message processing. But this processing can be + * postponed due aborted transaction. On second hand there is not a + * reason, why don't do it at transaction end immediately. + */ + if (varform->varxactendaction == VARIABLE_XACTEND_RESET || + varform->varxactendaction == VARIABLE_XACTEND_DROP) + { + svar->reset_at_eox = true; + has_session_variables_with_reset_at_eox = true; + } + else + svar->reset_at_eox = false; + svar->drop_lxid = InvalidTransactionId; svar->isnull = true; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 76655aa8aeb..d8de8acf944 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5259,6 +5259,7 @@ CreateSessionVarStmt: * transaction end like tables. */ XactEndActionOption: ON COMMIT DROP { $$ = VARIABLE_XACTEND_DROP; } + | ON TRANSACTION END_P RESET { $$ = VARIABLE_XACTEND_RESET; } | /*EMPTY*/ { $$ = VARIABLE_XACTEND_NOOP; } ; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b8d839ece41..2ec6a22a266 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5433,6 +5433,7 @@ getVariables(Archive *fout) int i_varnamespace; int i_vartype; int i_vartypname; + int i_varxactendaction; int i_varowner; int i_varcollation; int i_varacl; @@ -5450,6 +5451,7 @@ getVariables(Archive *fout) /* get the variables in current database */ appendPQExpBuffer(query, "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varxactendaction,\n" " v.varnamespace, v.vartype,\n" " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" " CASE WHEN v.varcollation <> t.typcollation " @@ -5472,6 +5474,7 @@ getVariables(Archive *fout) i_varnamespace = PQfnumber(res, "varnamespace"); i_vartype = PQfnumber(res, "vartype"); i_vartypname = PQfnumber(res, "vartypname"); + i_varxactendaction = PQfnumber(res, "varxactendaction"); i_varcollation = PQfnumber(res, "varcollation"); i_varowner = PQfnumber(res, "varowner"); @@ -5495,6 +5498,9 @@ getVariables(Archive *fout) varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varxactendaction = + pg_strdup(PQgetvalue(res, i, i_varxactendaction)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); @@ -5535,6 +5541,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) PQExpBuffer query; char *qualvarname; const char *vartypname; + const char *varxactendaction; Oid varcollation; /* skip if not to be dumped */ @@ -5546,6 +5553,7 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); vartypname = varinfo->vartypname; + varxactendaction = varinfo->varxactendaction; varcollation = varinfo->varcollation; appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", @@ -5564,6 +5572,9 @@ dumpVariable(Archive *fout, const VariableInfo *varinfo) fmtQualifiedDumpable(coll)); } + if (strcmp(varxactendaction, "r") == 0) + appendPQExpBuffer(query, " ON TRANSACTION END RESET"); + appendPQExpBuffer(query, ";\n"); if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f2f558b2961..7c0c03749e8 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -709,6 +709,7 @@ typedef struct _VariableInfo DumpableAcl dacl; Oid vartype; char *vartypname; + char *varxactendaction; char *varacl; char *rvaracl; char *initvaracl; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index dd8c054a6a6..419ea39727b 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3987,6 +3987,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable ON TRANSACTION END RESET' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable2 AS integer ON TRANSACTION END RESET;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d02f6416f0e..d0f69f8c8e9 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5229,6 +5229,7 @@ listVariables(const char *pattern, bool verbose) " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\",\n" " CASE v.varxactendaction\n" " WHEN 'd' THEN 'ON COMMIT DROP'\n" + " WHEN 'r' THEN 'ON TRANSACTION END RESET'\n" " END as \"%s\"\n", gettext_noop("Schema"), gettext_noop("Name"), diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 1e6dd98d415..60291ef7ac2 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -74,6 +74,7 @@ typedef enum VariableXactEndAction { VARIABLE_XACTEND_NOOP = 'n', /* NOOP */ VARIABLE_XACTEND_DROP = 'd', /* ON COMMIT DROP */ + VARIABLE_XACTEND_RESET = 'r', /* ON TRANSACTION END RESET */ } VariableXactEndAction; /* ---------------- diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out index a609797dc5d..e9a254bcd12 100644 --- a/src/test/isolation/expected/session-variable.out +++ b/src/test/isolation/expected/session-variable.out @@ -86,11 +86,12 @@ myvar step sr1: ROLLBACK; -starting permutation: create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +starting permutation: create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state step create3: CREATE VARIABLE myvar3 AS text; step let3: LET myvar3 = 'test'; step s3: BEGIN; step o_c_d: CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; +step o_eox_r: CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; step create4: CREATE VARIABLE myvar4 AS text; step let4: LET myvar4 = 'test'; step drop4: DROP VARIABLE myvar4; @@ -103,6 +104,7 @@ t step discard: DISCARD VARIABLES; step sc3: COMMIT; +step clean: DROP VARIABLE myvar_o_eox_r; step state: SELECT varname FROM pg_variable; varname ------- diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec index 45e65d4085d..5d089c8a4ed 100644 --- a/src/test/isolation/specs/session-variable.spec +++ b/src/test/isolation/specs/session-variable.spec @@ -25,12 +25,14 @@ session s3 step s3 { BEGIN; } step let3 { LET myvar3 = 'test'; } step o_c_d { CREATE TEMP VARIABLE myvar_o_c_d AS text ON COMMIT DROP; } +step o_eox_r { CREATE VARIABLE myvar_o_eox_r AS text ON TRANSACTION END RESET; LET myvar_o_eox_r = 'test'; } step create4 { CREATE VARIABLE myvar4 AS text; } step let4 { LET myvar4 = 'test'; } step drop4 { DROP VARIABLE myvar4; } step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } step discard { DISCARD VARIABLES; } step sc3 { COMMIT; } +step clean { DROP VARIABLE myvar_o_eox_r; } step state { SELECT varname FROM pg_variable; } session s4 @@ -48,4 +50,4 @@ permutation let val dbg drop create dbg val # calling the dbg step after the concurrent drop permutation let val s1 dbg drop create dbg val sr1 # test for DISCARD ALL when all internal queues have actions registered -permutation create3 let3 s3 o_c_d create4 let4 drop4 drop3 inval3 discard sc3 state +permutation create3 let3 s3 o_c_d o_eox_r create4 let4 drop4 drop3 inval3 discard sc3 clean state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 6dadb9074da..99e4ac72be1 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1520,3 +1520,37 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +COMMIT; +-- should be NULL; +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +BEGIN; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be NULL +SELECT var1 IS NULL; + ?column? +---------- + t +(1 row) + +DROP VARIABLE var1; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 770ce650685..7853261080b 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -1018,3 +1018,23 @@ COMMIT; SELECT count(*) FROM pg_variable WHERE varname = 'var1'; -- should be zero SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS int ON TRANSACTION END RESET; + +BEGIN; + LET var1 = 100; + SELECT var1; +COMMIT; + +-- should be NULL; +SELECT var1 IS NULL; + +BEGIN; + LET var1 = 100; + SELECT var1; +ROLLBACK; + +-- should be NULL +SELECT var1 IS NULL; + +DROP VARIABLE var1; -- 2.47.0 [text/x-patch] v20241120-0007-GUC-session_variables_ambiguity_warning.patch (13.9K, 16-v20241120-0007-GUC-session_variables_ambiguity_warning.patch) download | inline diff: From ce4db3e0f7cce2994cd21bbcac056f8fc459e9a6 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 17:55:24 +0200 Subject: [PATCH 07/21] GUC session_variables_ambiguity_warning Inside an query the session variables can be shadowed. This behaviour is by design, because this ensuring so new variables doesn't break existing queries. But this behaviour can be confusing, because shadowing is quiet. When new GUC session_variables_ambiguity_warning is on, then the warning is displayed, when some session variable can be used, but it is not used due shadowing. This feature should to help with investigation of unexpected behaviour. Note: PLpgSQL has an option that controls priority of identifier's spaces (SQL or PLpgSQL). Default behaviour doesn't allows collisions. I believe this default is the best. I cannot to implement similar very strict behaviour to session variables, because one unhappy named session variable can breaks an applications. The problems related to badly named PLpgSQL's varible is limitted just to only one routine. --- doc/src/sgml/config.sgml | 60 +++++++++++++++++++ doc/src/sgml/ddl.sgml | 10 ++++ src/backend/parser/parse_expr.c | 49 +++++++++++++-- src/backend/utils/misc/guc_tables.c | 9 +++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/parser/parse_expr.h | 1 + .../src/expected/plpgsql_session_variable.out | 33 ++++++++++ .../src/sql/plpgsql_session_variable.sql | 29 +++++++++ .../regress/expected/session_variables.out | 22 +++++++ src/test/regress/sql/session_variables.sql | 20 +++++++ 10 files changed, 230 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index a84e60c09b9..d1b2175c022 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10740,6 +10740,66 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir' </listitem> </varlistentry> + <varlistentry id="guc-session-variables-ambiguity-warning" xreflabel="session_variables_ambiguity_warning"> + <term><varname>session_variables_ambiguity_warning</varname> (<type>boolean</type>) + <indexterm> + <primary><varname>session_variables_ambiguity_warning</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + When on, a warning is raised when any identifier in a query could be + used as both a column identifier, routine variable or a session + variable identifier. The default is <literal>off</literal>. + </para> + <para> + Session variables can be shadowed by column references in a query, this + is an expected behavior. Previously working queries shouldn't error out + by creating any session variable, so session variables are always shadowed + if an identifier is ambiguous. Variables should be referenced using + anunambiguous identifier without any possibility for a collision with + identifier of other database objects (column names or record fields names). + The warning messages emitted when enabling <varname>session_variables_ambiguity_warning</varname> + can help finding such identifier collision. +<programlisting> +CREATE TABLE foo(a int); +INSERT INTO foo VALUES(10); +CREATE VARIABLE a int; +LET a = 100; +SELECT a FROM foo; +</programlisting> + +<screen> + a +---- + 10 +(1 row) +</screen> + +<programlisting> +SET session_variables_ambiguity_warning TO on; +SELECT a FROM foo; +</programlisting> + +<screen> +WARNING: session variable "a" is shadowed +LINE 1: SELECT a FROM foo; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + a +---- + 10 +(1 row) +</screen> + </para> + <para> + This feature can significantly increase log size, so it's disabled by + default. For testing or development environments it's recommended to + enable it if you use session variables. + </para> + </listitem> + </varlistentry> + <varlistentry id="guc-standard-conforming-strings" xreflabel="standard_conforming_strings"> <term><varname>standard_conforming_strings</varname> (<type>boolean</type>) <indexterm><primary>strings</primary><secondary>standard conforming</secondary></indexterm> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 327548fae12..ce1b1e1f599 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5359,6 +5359,16 @@ SELECT current_user_id; the schema name, columns can use table aliases, routine variables can use block labels, and routine arguments can use the routine name. </para> + + <para> + When a query contains identifiers or qualified identifiers that could be + used as both a session variable identifiers and as column identifier, + then the column identifier is preferred every time. Warnings can be + emitted when this situation happens by enabling configuration parameter <xref + linkend="guc-session-variables-ambiguity-warning"/>. User can explicitly + qualify the source object by syntax <literal>table.column</literal> or + <literal>schema.variable</literal>. + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index aba5259e027..3602c3572d7 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -45,6 +45,7 @@ /* GUC parameters */ bool Transform_null_equals = false; +bool session_variables_ambiguity_warning = false; static Node *transformExprRecurse(ParseState *pstate, Node *expr); @@ -960,11 +961,51 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) * * SELECT foo.a, foo.b, foo.c FROM foo; * - * However, that is very confusing, so we disallow it. We don't try to - * identify a variable if we know that it would be shadowed. - * ----- + * However, that is very confusing, so we disallow it. + * + * When session_variables_ambiguity_warning is requested, then we + * need to identify a variable although we know, so this variable + * would be shadowed. */ - if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + if (node || (relname && crerr == CRERR_NO_COLUMN)) + { + /* + * In this path we just try (if it is wanted) detect if session + * variable is shadowed. + */ + if (session_variables_ambiguity_warning) + { + /* + * The AccessShareLock is created on related session variable. + * The lock will be kept for the whole transaction. + */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, true); + + if (OidIsValid(varid)) + { + /* this path will ending by WARNING. Unlock variable first */ + UnlockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (node) + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s\" is shadowed", + NameListToString(cref->fields)), + errdetail("Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name."), + parser_errposition(pstate, cref->location))); + else + /* session variable is shadowed by RTE */ + ereport(WARNING, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("session variable \"%s.%s\" is shadowed", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + errdetail("Session variables can be shadowed by tables or table's aliases with the same name."), + parser_errposition(pstate, cref->location))); + } + } + } + else { /* takes an AccessShareLock on the session variable */ varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 8a67f01200c..efb65b02380 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -1595,6 +1595,15 @@ struct config_bool ConfigureNamesBool[] = false, NULL, NULL, NULL }, + { + {"session_variables_ambiguity_warning", PGC_USERSET, CLIENT_CONN_OTHER, + gettext_noop("Raise a warning when reference to a session variable is ambiguous."), + NULL + }, + &session_variables_ambiguity_warning, + false, + NULL, NULL, NULL + }, { {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, gettext_noop("Sets the default read-only status of new transactions."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 39a3ac23127..992dda3c644 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -714,6 +714,7 @@ #default_transaction_read_only = off #default_transaction_deferrable = off #session_replication_role = 'origin' +#session_variables_ambiguity_warning = off #statement_timeout = 0 # in milliseconds, 0 is disabled #transaction_timeout = 0 # in milliseconds, 0 is disabled #lock_timeout = 0 # in milliseconds, 0 is disabled diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index 9b46dfd9ecc..0d64b300f8a 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -17,6 +17,7 @@ /* GUC parameters */ extern PGDLLIMPORT bool Transform_null_equals; +extern PGDLLIMPORT bool session_variables_ambiguity_warning; extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind); diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out index ab779900596..a6523f7afe2 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -354,6 +354,39 @@ $$; NOTICE: session variable is 100 NOTICE: plpgsql variable is 1000 NOTICE: variable is 1000 +-- againt with session_variables_ambiguity_warning(on) +SET session_variables_ambiguity_warning TO on; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +WARNING: session variable "plpgsql_sv_var1" is shadowed +LINE 1: plpgsql_sv_var1 + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. +QUERY: plpgsql_sv_var1 +NOTICE: variable is 1000 +SET session_variables_ambiguity_warning TO off; DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted CREATE VARIABLE plpgsql_sv_v text; diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql index a3cc264cf38..9b8e90e81fa 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -260,6 +260,35 @@ BEGIN END; $$; +-- againt with session_variables_ambiguity_warning(on) + +SET session_variables_ambiguity_warning TO on; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + -- should be ok without warning + plpgsql_sv_var1 := 1000; + + -- should be ok without warning + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- should be ok without warning + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- should to print plpgsql variable with warning + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +SET session_variables_ambiguity_warning TO off; + DROP VARIABLE plpgsql_sv_var1; -- the value should not be corrupted diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 85771032de3..50fb478b5e9 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1261,3 +1261,25 @@ SELECT var1; (1 row) DROP VARIABLE var1, var2; +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; +CREATE VARIABLE xxtab.avar int; +CREATE TABLE public.xxtab(avar int); +INSERT INTO public.xxtab VALUES(1); +LET xxtab.avar = 20; +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; +WARNING: session variable "xxtab.avar" is shadowed +LINE 1: SELECT xxtab.avar FROM public.xxtab; + ^ +DETAIL: Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name. + avar +------ + 1 +(1 row) + +SET session_variables_ambiguity_warning TO off; +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; +NOTICE: drop cascades to session variable xxtab.avar diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index d8397573eeb..e7a4fdedae4 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -854,3 +854,23 @@ BEGIN; DROP VARIABLE var1; ROLLBACK; SELECT var1; DROP VARIABLE var1, var2; + +-- test session_variables_ambiguity_warning +CREATE SCHEMA xxtab; + +CREATE VARIABLE xxtab.avar int; + +CREATE TABLE public.xxtab(avar int); + +INSERT INTO public.xxtab VALUES(1); + +LET xxtab.avar = 20; + +SET session_variables_ambiguity_warning TO on; +--- should to raise warning, show 1 +SELECT xxtab.avar FROM public.xxtab; + +SET session_variables_ambiguity_warning TO off; + +DROP TABLE public.xxtab; +DROP SCHEMA xxtab CASCADE; -- 2.47.0 [text/x-patch] v20241120-0008-EXPLAIN-LET-support.patch (8.2K, 17-v20241120-0008-EXPLAIN-LET-support.patch) download | inline diff: From ac4d660266a48bc2b6282ef58b5099918825ff91 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 21 May 2024 18:28:07 +0200 Subject: [PATCH 08/21] EXPLAIN LET support Enhancing ExplainOnePlan is necessary to be EXPLAIN ANALYZE LET fully workable. In this case we want to be result of query or expression written to target variable. --- doc/src/sgml/ref/explain.sgml | 3 +- src/backend/commands/explain.c | 31 ++++++++++++-- src/backend/commands/prepare.c | 5 ++- src/backend/parser/gram.y | 3 +- src/include/commands/explain.h | 3 +- .../regress/expected/session_variables.out | 40 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 21 ++++++++++ 7 files changed, 97 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml index db9d3a8549a..caccc70658c 100644 --- a/doc/src/sgml/ref/explain.sgml +++ b/doc/src/sgml/ref/explain.sgml @@ -98,7 +98,8 @@ EXPLAIN [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] <rep <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>, <command>MERGE</command>, <command>CREATE TABLE AS</command>, - or <command>EXECUTE</command> statement + <command>EXECUTE</command>, + or <command>LET</command> statement without letting the command affect your data, use this approach: <programlisting> BEGIN; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 7c0fd63b2f0..d41516454f5 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -18,6 +18,7 @@ #include "commands/createas.h" #include "commands/defrem.h" #include "commands/prepare.h" +#include "executor/svariableReceiver.h" #include "foreign/fdwapi.h" #include "jit/jit.h" #include "libpq/pqformat.h" @@ -512,8 +513,9 @@ standard_ExplainOneQuery(Query *query, int cursorOptions, } /* run it (if needed) and produce output */ - ExplainOnePlan(plan, into, es, queryString, params, queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(plan, into, query->resultVariable, es, queryString, + params, queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); } @@ -611,6 +613,25 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, else ExplainDummyGroup("Notify", NULL, es); } + else if (IsA(utilityStmt, LetStmt)) + { + LetStmt *letstmt = (LetStmt *) utilityStmt; + List *rewritten; + Query *query; + + if (es->format == EXPLAIN_FORMAT_TEXT) + appendStringInfoString(es->str, "SET SESSION VARIABLE\n"); + else + ExplainDummyGroup("Set Session Variable", NULL, es); + + rewritten = QueryRewrite(castNode(Query, copyObject(letstmt->query))); + + Assert(list_length(rewritten) == 1); + query = linitial_node(Query, rewritten); + ExplainOneQuery(query, + CURSOR_OPT_PARALLEL_OK, NULL, es, + pstate, params); + } else { if (es->format == EXPLAIN_FORMAT_TEXT) @@ -634,8 +655,8 @@ ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, * to call it. */ void -ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, - const char *queryString, ParamListInfo params, +ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, Oid targetvar, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, const BufferUsage *bufusage, const MemoryContextCounters *mem_counters) @@ -684,6 +705,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, */ if (into) dest = CreateIntoRelDestReceiver(into); + else if (OidIsValid(targetvar)) + dest = CreateVariableDestReceiver(targetvar); else if (es->serialize != EXPLAIN_SERIALIZE_NONE) dest = CreateExplainSerializeDestReceiver(es); else diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 455a03cce63..3735b5554e0 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -662,8 +662,9 @@ ExplainExecuteQuery(ExecuteStmt *execstmt, IntoClause *into, ExplainState *es, PlannedStmt *pstmt = lfirst_node(PlannedStmt, p); if (pstmt->commandType != CMD_UTILITY) - ExplainOnePlan(pstmt, into, es, query_string, paramLI, pstate->p_queryEnv, - &planduration, (es->buffers ? &bufusage : NULL), + ExplainOnePlan(pstmt, into, InvalidOid, es, query_string, paramLI, + pstate->p_queryEnv, &planduration, + (es->buffers ? &bufusage : NULL), es->memory ? &mem_counters : NULL); else ExplainOneUtility(pstmt->utilityStmt, into, es, pstate, paramLI); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index aad47305f20..def7dde2330 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -12130,7 +12130,8 @@ ExplainableStmt: | CreateAsStmt | CreateMatViewStmt | RefreshMatViewStmt - | ExecuteStmt /* by default all are $$=$1 */ + | ExecuteStmt + | LetStmt /* by default all are $$=$1 */ ; /***************************************************************************** diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index aa5872bc154..f5b08857ae8 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -103,7 +103,8 @@ extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into, ExplainState *es, ParseState *pstate, ParamListInfo params); -extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, +extern void ExplainOnePlan(PlannedStmt *plannedstmt, + IntoClause *into, Oid targetvar, ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 50fb478b5e9..a867fdd3e75 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1283,3 +1283,43 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; NOTICE: drop cascades to session variable xxtab.avar +CREATE VARIABLE var1 bigint; +CREATE TABLE var_tab_test_table(a int); +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); +VACUUM ANALYZE var_tab_test_table; +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) LET var1 = (SELECT count(*) FROM var_tab_test_table); + 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) + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index e7a4fdedae4..8a6dbffd54e 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -874,3 +874,24 @@ SET session_variables_ambiguity_warning TO off; DROP TABLE public.xxtab; DROP SCHEMA xxtab CASCADE; + +CREATE VARIABLE var1 bigint; + +CREATE TABLE var_tab_test_table(a int); + +INSERT INTO var_tab_test_table SELECT * FROM generate_series(1,10); + +VACUUM ANALYZE var_tab_test_table; + +EXPLAIN (COSTS OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be NULL +SELECT var1; + +EXPLAIN (COSTS OFF, TIMING OFF, ANALYZE, SUMMARY OFF) LET var1 = (SELECT count(*) FROM var_tab_test_table); + +-- should be 10 +SELECT var1; + +DROP VARIABLE var1; +DROP TABLE var_tab_test_table; -- 2.47.0 [text/x-patch] v20241120-0005-memory-cleaning-after-DROP-VARIABLE.patch (20.8K, 18-v20241120-0005-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From e711b4cd780883f846e74fcae80f1ddcc3a161c0 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 09:31:28 +0100 Subject: [PATCH 05/21] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 4 + src/backend/commands/session_variable.c | 153 +++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../regress/expected/session_variables.out | 217 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 120 ++++++++++ 8 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index ef89594a268..c7369e2b2ac 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/builtins.h" @@ -254,4 +255,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 668b8189a6c..21c7a9517b5 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "commands/session_variable.h" #include "executor/svariableReceiver.h" @@ -67,6 +68,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -83,6 +92,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -114,6 +134,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -167,6 +219,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -196,6 +309,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -319,22 +434,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -395,6 +530,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 443afbafd4a..3dab8ae2a48 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "tcop/cmdtag.h" #include "utils/queryenvironment.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..0a5579dc7ce --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT myvar; +ERROR: column "myvar" does not exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT myvar; +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step val: SELECT myvar; +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 143109aa4da..7453685d046 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -115,3 +115,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..c864fee4006 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT myvar; } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index 2c4760fb687..85771032de3 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -1044,3 +1044,220 @@ SELECT count(*) FROM pg_session_variables(); 0 (1 row) +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; +-- should fail +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + Ahoj +(1 row) + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; + var1 +------ + 100 +(1 row) + +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + var1 +------ + Ahoj +(1 row) + +DROP VARIABLE var1; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + var1 +------ + 100 +(1 row) + + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+---------+------------+------------ + var1 | integer | t | t +(1 row) + + DROP VARIABLE var1; +COMMIT; +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + var2 +------ + 0 +(1 row) + +ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; + var2 +------ + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +DROP VARIABLE var1, var2; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 979b9029e58..d8397573eeb 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -734,3 +734,123 @@ DISCARD VARIABLES; -- should be zero again SELECT count(*) FROM pg_session_variables(); + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET var1 = 'Ahoj'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Ahoj'; +BEGIN; +DROP VARIABLE var1; + +-- should fail +SELECT var1; + +ROLLBACK; + +-- should be ok +SELECT var1; + +-- another test +BEGIN; +DROP VARIABLE var1; +CREATE VARIABLE var1 AS int; +LET var1 = 100; +-- should be ok, result 100 +SELECT var1; +ROLLBACK; +-- should be ok, result 'Ahoj' +SELECT var1; + +DROP VARIABLE var1; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +BEGIN; + CREATE VARIABLE var1 AS int; + LET var1 = 100; + SELECT var1; + SELECT name, typname, can_select, can_update FROM pg_session_variables(); + DROP VARIABLE var1; +COMMIT; + +-- should be zero +SELECT count(*) FROM pg_session_variables() WHERE NOT removed; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int; +LET var1 = 10; +LET var2 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + SAVEPOINT s2; + DROP VARIABLE var1; + SELECT var2; +ROLLBACK; +-- should be ok +SELECT var1; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE var1; + -- force cleaning by touching another session variable + SELECT var2; + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT var2; + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT var2; +COMMIT; +-- should be ok +SELECT var1; + +-- repeated aborted transaction +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; +BEGIN; DROP VARIABLE var1; ROLLBACK; + +-- should be ok +SELECT var1; + +DROP VARIABLE var1, var2; -- 2.47.0 [text/x-patch] v20241120-0004-DISCARD-VARIABLES.patch (9.6K, 19-v20241120-0004-DISCARD-VARIABLES.patch) download | inline diff: From 7b519df9d91060c6b2a2ef964eac82755682a82e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 28 Jan 2024 20:54:20 +0100 Subject: [PATCH 04/21] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 +++- src/backend/commands/discard.c | 6 ++ src/backend/commands/session_variable.c | 28 ++++++++- src/backend/parser/gram.y | 6 ++ src/backend/tcop/utility.c | 3 + src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../regress/expected/session_variables.out | 63 +++++++++++++++++++ src/test/regress/sql/session_variables.sql | 36 +++++++++++ 11 files changed, 158 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 92d983ac748..d06ebaba6f4 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 13ab9575fa9..668b8189a6c 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -94,7 +94,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -661,3 +667,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8ae368c513c..aad47305f20 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2077,7 +2077,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 69d2f7e1ced..970451460c2 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2962,6 +2962,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec572815c94..3ef4776d98a 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4083,7 +4083,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index b3f03c65827..443afbafd4a 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expect extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 778db6ad4f2..04ab22bd492 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -3988,6 +3988,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index a921af2486f..bd7964aea6e 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index a2cb35e3d7d..2c4760fb687 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -981,3 +981,66 @@ DROP VARIABLE :"DBNAME".:"DBNAME".b; DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + var1 +------- + Hello +(1 row) + +DISCARD ALL; +SELECT var1; + var1 +------ + +(1 row) + +LET var1 = 'AHOJ'; +SELECT var1; + var1 +------ + AHOJ +(1 row) + +DISCARD VARIABLES; +SELECT var1; + var1 +------ + +(1 row) + +DROP VARIABLE var1; +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +CREATE VARIABLE var1 AS varchar; +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +LET var1 = 'AHOJ'; +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + name | typname | can_select | can_update +------+-------------------+------------+------------ + var1 | character varying | t | t +(1 row) + +DISCARD VARIABLES; +-- should be zero again +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 6445479d22d..979b9029e58 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -698,3 +698,39 @@ DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; DROP SCHEMA :"DBNAME"; RESET search_path; + +-- memory cleaning by DISCARD command +CREATE VARIABLE var1 AS varchar; +LET var1 = 'Hello'; +SELECT var1; + +DISCARD ALL; +SELECT var1; + +LET var1 = 'AHOJ'; +SELECT var1; + +DISCARD VARIABLES; +SELECT var1; + +DROP VARIABLE var1; + +-- initial test of debug pg_session_variables function +-- should be zero now +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +CREATE VARIABLE var1 AS varchar; + +-- should be zero still +SELECT count(*) FROM pg_session_variables(); + +LET var1 = 'AHOJ'; + +SELECT name, typname, can_select, can_update FROM pg_session_variables(); + +DISCARD VARIABLES; + +-- should be zero again +SELECT count(*) FROM pg_session_variables(); -- 2.47.0 [text/x-patch] v20241120-0006-plpgsql-tests.patch (16.9K, 20-v20241120-0006-plpgsql-tests.patch) download | inline diff: From 5539a37825205becdc38aeb1d8ea1b48cf0a9dd3 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 06/21] plpgsql tests --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 382 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 289 +++++++++++++ 4 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ab779900596 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,382 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; +LET plpgsql_sv_var = pi(); +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; +NOTICE: value of session variable is 3.14159265358979 +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; +SELECT plpgsql_sv_var AS "pi_multiply_2"; + pi_multiply_2 +------------------ + 6.28318530717959 +(1 row) + +DROP VARIABLE plpgsql_sv_var; +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; +-- execute in a transaction +BEGIN; +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +END; +-- execute outside of a transaction +SELECT writer_func(); + writer_func +------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 + reader_func +------------- + +(1 row) + +SELECT updater_func(); + updater_func +-------------- + +(1 row) + +SELECT reader_func(); +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 + reader_func +------------- + +(1 row) + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; +NOTICE: var1 = 10 +NOTICE: var2 = 3.14159265358979 +NOTICE: length of var3 = 10002 +NOTICE: var1 = 110 +NOTICE: var2 = 100000000003.14159265358979 +NOTICE: length of var3 = 20004 +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; +SELECT test_func(); +NOTICE: {100} +NOTICE: {-1} + test_func +----------- + +(1 row) + +DROP FUNCTION test_func(); +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +SET ROLE TO regress_var_owner_role; +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_reader_role; +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail +SELECT var_read_func(); +ERROR: permission denied for session variable plpgsql_sv_var1 +CONTEXT: PL/pgSQL expression "plpgsql_sv_var1" +PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should be ok +SELECT var_read_func(); +NOTICE: 10 + var_read_func +--------------- + +(1 row) + +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_owner_role; +DROP VARIABLE plpgsql_sv_var1; +SET ROLE TO DEFAULT; +SET ROLE TO regress_var_exec_role; +-- should fail, but not crash +SELECT var_read_func(); +ERROR: column "plpgsql_sv_var1" does not exist +LINE 1: plpgsql_sv_var1 + ^ +QUERY: plpgsql_sv_var1 +CONTEXT: PL/pgSQL function var_read_func() line 3 at RAISE +SET ROLE TO DEFAULT; +DROP FUNCTION var_read_func; +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_int(1); + inc_var_int +------------- + 1 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 2 +(1 row) + +SELECT inc_var_int(1); + inc_var_int +------------- + 3 +(1 row) + +SELECT inc_var_int(1) FROM generate_series(1,10); + inc_var_int +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +LET plpgsql_sv_var2 = 0.0; +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; +SELECT inc_var_num(1.0); + inc_var_num +------------- + 1 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 2 +(1 row) + +SELECT inc_var_num(1.0); + inc_var_num +------------- + 3 +(1 row) + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + inc_var_num +------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; +NOTICE: session variable is 100 +NOTICE: plpgsql variable is 1000 +NOTICE: variable is 1000 +DROP VARIABLE plpgsql_sv_var1; +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +select ffunc(); + ffunc +------- + abc +(1 row) + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); +DROP VARIABLE plpgsql_sv_v; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 3dd734b776b..3905c0b6d45 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..a3cc264cf38 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,289 @@ +-- test of session variables +CREATE VARIABLE plpgsql_sv_var AS numeric; + +LET plpgsql_sv_var = pi(); + +-- passing parameters to DO block +DO $$ +BEGIN + RAISE NOTICE 'value of session variable is %', plpgsql_sv_var; +END; +$$; + +-- passing output from DO block; +DO $$ +BEGIN + LET plpgsql_sv_var = 2 * pi(); +END +$$; + +SELECT plpgsql_sv_var AS "pi_multiply_2"; + +DROP VARIABLE plpgsql_sv_var; + +-- test access from PL/pgSQL +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +CREATE OR REPLACE FUNCTION writer_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = 10; + LET plpgsql_sv_var2 = pi(); + -- very long value + LET plpgsql_sv_var3 = format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION updater_func() +RETURNS void AS $$ +BEGIN + LET plpgsql_sv_var1 = plpgsql_sv_var1 + 100; + LET plpgsql_sv_var2 = plpgsql_sv_var2 + 100000000000; + -- very long value + LET plpgsql_sv_var3 = plpgsql_sv_var3 || format('(%s)', repeat('*', 10000)); +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION reader_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE 'var1 = %', plpgsql_sv_var1; + RAISE NOTICE 'var2 = %', plpgsql_sv_var2; + RAISE NOTICE 'length of var3 = %', length(plpgsql_sv_var3); +END; +$$ LANGUAGE plpgsql; + +-- execute in a transaction +BEGIN; +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); +END; + +-- execute outside of a transaction +SELECT writer_func(); +SELECT reader_func(); +SELECT updater_func(); +SELECT reader_func(); + +-- execute inside a PL/pgSQL block +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +-- plan caches should be correctly invalidated +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS numeric; +CREATE VARIABLE plpgsql_sv_var3 AS varchar; + +-- should work again +DO $$ +BEGIN + PERFORM writer_func(); + PERFORM reader_func(); + PERFORM updater_func(); + PERFORM reader_func(); +END; +$$; + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2, plpgsql_sv_var3; + +DROP FUNCTION writer_func; +DROP FUNCTION reader_func; +DROP FUNCTION updater_func; + +-- another check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +CREATE OR REPLACE FUNCTION test_func() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sv_var1 = 1; + v[plpgsql_sv_var1] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sv_var2 = v; + LET plpgsql_sv_var2[plpgsql_sv_var1] = -1; + RAISE NOTICE '%', plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +CREATE VARIABLE plpgsql_sv_var2 AS int[]; + +SELECT test_func(); + +DROP FUNCTION test_func(); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +-- check secure access +CREATE ROLE regress_var_owner_role; +CREATE ROLE regress_var_reader_role; +CREATE ROLE regress_var_exec_role; + +GRANT ALL ON SCHEMA public TO regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +SET ROLE TO regress_var_owner_role; + +CREATE VARIABLE plpgsql_sv_var1 AS int; +LET plpgsql_sv_var1 = 10; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_reader_role; + +CREATE OR REPLACE FUNCTION var_read_func() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; +GRANT SELECT ON VARIABLE plpgsql_sv_var1 TO regress_var_reader_role; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should be ok +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_owner_role; + +DROP VARIABLE plpgsql_sv_var1; + +SET ROLE TO DEFAULT; + +SET ROLE TO regress_var_exec_role; + +-- should fail, but not crash +SELECT var_read_func(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION var_read_func; + +REVOKE ALL ON SCHEMA public FROM regress_var_owner_role, regress_var_reader_role, regress_var_exec_role; + +DROP ROLE regress_var_owner_role; +DROP ROLE regress_var_reader_role; +DROP ROLE regress_var_exec_role; + +-- returns updated value +CREATE VARIABLE plpgsql_sv_var1 AS int; + +CREATE OR REPLACE FUNCTION inc_var_int(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var1 = COALESCE(plpgsql_sv_var1 + $1, $1); + RETURN plpgsql_sv_var1; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_int(1); +SELECT inc_var_int(1); +SELECT inc_var_int(1); + +SELECT inc_var_int(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sv_var2 AS numeric; + +LET plpgsql_sv_var2 = 0.0; + +CREATE OR REPLACE FUNCTION inc_var_num(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sv_var2 = COALESCE(plpgsql_sv_var2 + $1, $1); + RETURN plpgsql_sv_var2; +END; +$$ LANGUAGE plpgsql; + +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); +SELECT inc_var_num(1.0); + +SELECT inc_var_num(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sv_var1, plpgsql_sv_var2; + +DROP FUNCTION inc_var_int; +DROP FUNCTION inc_var_num; + +-- plpgsql variables are preferred against session variables +CREATE VARIABLE plpgsql_sv_var1 AS int; + +DO $$ +<<myblock>> +DECLARE plpgsql_sv_var1 int; +BEGIN + LET plpgsql_sv_var1 = 100; + + plpgsql_sv_var1 := 1000; + + -- print 100; + RAISE NOTICE 'session variable is %', public.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'plpgsql variable is %', myblock.plpgsql_sv_var1; + + -- print 1000 + RAISE NOTICE 'variable is %', plpgsql_sv_var1; +END; +$$; + +DROP VARIABLE plpgsql_sv_var1; + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sv_v text; +LET plpgsql_sv_v = 'abc'; + +CREATE FUNCTION ffunc() +RETURNS text AS $$ +BEGIN + RETURN gfunc(plpgsql_sv_v); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION gfunc(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sv_v = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +select ffunc(); + +DROP FUNCTION ffunc(); +DROP FUNCTION gfunc(text); + +DROP VARIABLE plpgsql_sv_v; -- 2.47.0 [text/x-patch] v20241120-0003-function-pg_session_variables-for-cleaning-tests.patch (4.3K, 21-v20241120-0003-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 21c87ec1ca6e5bb934cb5cc3a9a093e4c4cae21f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 03/21] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 92 +++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 +++ 2 files changed, 100 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 548d9835c0d..13ab9575fa9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -569,3 +569,95 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 41f1121d19f..e943a74846f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12405,4 +12405,12 @@ proargtypes => 'int2', prosrc => 'gist_stratnum_identity' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.47.0 [text/x-patch] v20241120-0002-Storage-for-session-variables-and-SQL-interface.patch (145.2K, 22-v20241120-0002-Storage-for-session-variables-and-SQL-interface.patch) download | inline diff: From 69f8cec63863ce2b0c6a43d01653cf00067eacc0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Thu, 6 Jul 2023 08:29:21 +0200 Subject: [PATCH 02/21] Storage for session variables and SQL interface Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. The identifiers of session variables should always be shadowed by possible column identifiers: we don't want to break an application by creating some badly named session variable. The limits of this patch (solved by other patches): - session variables block parallel execution - session variables blocks simple expression evaluation (in plpgsql) - SQL functions with session variables are not inlined - CALL statement is not supported (usage of direct access to express executor) - EXECUTE statement is not supported (usage of direct access to express executor) - memory used by dropped session variables is not released Implementations of EXPLAIN LET and PREPARE LET statements are in separate patches (for better readability) --- doc/src/sgml/catalogs.sgml | 13 + doc/src/sgml/ddl.sgml | 32 + doc/src/sgml/parallel.sgml | 6 + doc/src/sgml/plpgsql.sgml | 14 + doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 1 + doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++ doc/src/sgml/reference.sgml | 1 + src/backend/catalog/dependency.c | 5 + src/backend/catalog/namespace.c | 315 ++++++ src/backend/catalog/pg_variable.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/prepare.c | 8 + src/backend/commands/session_variable.c | 571 +++++++++++ src/backend/executor/Makefile | 1 + src/backend/executor/execExpr.c | 36 + src/backend/executor/execMain.c | 57 ++ src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 201 ++++ src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 9 + src/backend/optimizer/plan/setrefs.c | 135 ++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 35 +- src/backend/parser/analyze.c | 271 ++++- src/backend/parser/gram.y | 49 +- src/backend/parser/parse_agg.c | 9 + src/backend/parser/parse_expr.c | 220 ++++- src/backend/parser/parse_func.c | 2 + src/backend/tcop/dest.c | 7 + src/backend/tcop/pquery.c | 3 + src/backend/tcop/utility.c | 16 + src/backend/utils/adt/ruleutils.c | 46 + src/backend/utils/cache/plancache.c | 41 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/namespace.h | 2 + src/include/catalog/pg_variable.h | 8 + src/include/commands/session_variable.h | 32 + src/include/executor/execdesc.h | 4 + src/include/executor/svariableReceiver.h | 22 + src/include/nodes/execnodes.h | 16 + src/include/nodes/parsenodes.h | 17 + src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 4 +- src/include/nodes/primnodes.h | 6 + src/include/optimizer/planmain.h | 2 + src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 3 + src/include/tcop/cmdtaglist.h | 1 + src/include/tcop/dest.h | 1 + src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 925 ++++++++++++++++++ src/test/regress/sql/session_variables.sql | 639 ++++++++++++ src/tools/pgindent/typedefs.list | 5 + 58 files changed, 3916 insertions(+), 23 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/include/executor/svariableReceiver.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index bdd2ca0152f..0c15d56dd42 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9785,6 +9785,19 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </para></entry> </row> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>XLogRecPtr</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + <row> <entry role="catalog_table_entry"><para role="column_definition"> <structfield>varname</structfield> <type>name</type> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2e3f0b95fb0..327548fae12 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5317,6 +5317,38 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; VARIABLE</command> command. </para> + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT READ ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); +SELECT current_user_id; +</programlisting> + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> + <para> Inside a query or an expression, a session variable can be <quote>shadowed</quote> by a column with the same name. Similarly, the diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 78e4983139b..bfbff9ab74e 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6034,6 +6034,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index d87570e7d8b..d2036351e53 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index ec165ab96fc..05bf67e3411 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -145,6 +145,7 @@ SELECT var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..f6640576bee --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>session_variable</literal></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>sql_expression</literal></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + + <para> + Example: +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index c9d686665de..148719e490e 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 86e2258657d..487353ef2eb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3412,6 +3412,321 @@ LookupVariable(const char *nspname, return varoid; } +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} + +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or "variable"."field". + * Check both variants, and returns InvalidOid with + * not_unique flag, when both interpretations are + * possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * In this case, the field1 should be variable name. But + * direct unboxing of composite session variables is not + * supported now, and then we don't need to try lookup + * related variable. + * + * Unboxing is supported by syntax (var).* + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.field. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else if (OidIsValid(varid)) + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index e3a861c8cba..ef89594a268 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -26,6 +26,7 @@ #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/pg_lsn.h" #include "utils/syscache.h" static ObjectAddress create_variable(const char *varName, @@ -101,6 +102,7 @@ create_variable(const char *varName, varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 48f7348f91c..1cfaeca51ea 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -50,6 +50,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 6dd00a4abde..ca621be5ecb 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -38,6 +38,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index a93f970a292..455a03cce63 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -338,6 +338,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..548d9835c0d --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,571 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "rewrite/rewriteHandler.h" +#include "storage/lmgr.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Wrapper around SetSessionVariable with permission checks. + */ +void +SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull) +{ + AclResult aclresult; + + /* is the caller allowed to update the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + SetSessionVariable(varid, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull, Oid *typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message ws processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + Assert(svar); + + *typid = svar->typid; + + return copy_session_variable_value(svar, isNull); +} + +/* + * Returns a copy of the value of the session variable after checking if the + * type is the same as "expected_typid". The caller is responsible for + * permission checks. + */ +Datum +GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid) +{ + SVariable svar; + + svar = get_session_variable(varid); + + Assert(svar && svar->is_valid); + + if (expected_typid != svar->typid) + elog(ERROR, "type of variable \"%s.%s\" is different than expected", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)); + + return copy_session_variable_value(svar, isNull); +} + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L, true); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 8f7a5340059..6dc8c562bec 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -34,6 +34,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -991,6 +992,41 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.d.param.paramtype = param->paramtype; ExprEvalPushStep(state, &scratch); break; + + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + if (var->typid != param->paramtype) + elog(ERROR, "type of buffered value is different than PARAM_VARIABLE type"); + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; + case PARAM_EXTERN: /* diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 5ca856fd279..21e938a6227 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/nodeSubplan.h" @@ -195,6 +197,61 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + ListCell *lc; + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach(lc, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + Oid varid = lfirst_oid(lc); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].varid = varid; + estate->es_session_variables[i].value = GetSessionVariable(varid, + &estate->es_session_variables[i].isnull, + &estate->es_session_variables[i].typid); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index b511a429adf..b6662c6e8fc 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c4cff44ecf4 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,201 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. Because a received tuple can have + * deleted attributes, we need to find the first non-deleted attribute + * (field "slot_offset"). The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int slot_offset; /* position of non-deleted attribute */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + int natts = typeinfo->natts; + int outcols = 0; + int i; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + for (i = 0; i < natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(typeinfo, i); + Oid typid; + Oid collid; + int32 typmod; + + if (attr->attisdropped) + continue; + + if (++outcols > 1) + continue; + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + /* + * Double check - the type and typmod of target variable should be the + * same as the type and typmod of assignment expression. The + * expression should be wrapped by a cast to the target type/typmod. + */ + if (attr->atttypid != typid || + (attr->atttypmod >= 0 && + attr->atttypmod != typmod)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("target session variable is of type %s" + " but expression is of type %s", + format_type_with_typemod(typid, typmod), + format_type_with_typemod(attr->atttypid, + attr->atttypmod)))); + + myState->need_detoast = attr->attlen == -1; + myState->slot_offset = i; + } + + if (outcols != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + outcols, + outcols))); + + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[myState->slot_offset]; + isnull = slot->tts_isnull[myState->slot_offset]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 3060847b133..8fc67f08d76 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4341,6 +4341,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 1f78dc3d530..aee3a0d1ad4 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -325,6 +325,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->lastPlanNodeId = 0; glob->transientPlan = false; glob->dependsOnRole = false; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -559,6 +560,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + result->sessionVariables = glob->sessionVariables; result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -729,6 +731,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 91c7c4fe2fe..1fa2ecf5c20 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1310,6 +1313,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -1958,8 +2005,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2049,6 +2097,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2058,6 +2113,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2076,6 +2135,41 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + ListCell *lc; + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach(lc, root->glob->sessionVariables) + { + if (lfirst_oid(lc) == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2137,7 +2231,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2159,7 +2256,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3544,6 +3642,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list, or as the target of a LET statement. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries @@ -3629,9 +3746,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3654,6 +3771,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, (void *) context, 0); diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 33b10d72cb5..9f698a46d87 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1410,6 +1410,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8e39795e245..a5452c0c9e4 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -935,6 +936,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2389,6 +2397,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2515,6 +2524,29 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariableWithTypeCheck(param->paramvarid, + &isnull, + param->paramtype); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4718,7 +4750,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 506e0631615..81b8b0a633d 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,9 +25,12 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -51,6 +54,7 @@ #include "utils/backend_status.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -83,6 +87,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -414,6 +420,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -493,6 +500,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -545,6 +557,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -653,6 +666,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1079,6 +1093,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1544,6 +1559,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1770,12 +1786,242 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + AclResult aclresult; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("type \"%s\" of target session variable \"%s.%s\" is not a composite type", + format_type_be(typid), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, stmt->location))); + + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, NameListToString(names)); + + pstate->p_expr_kind = EXPR_KIND_LET_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach(lc, selectQuery->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Param *param; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s," + " but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. This value is used multiple times: by + * the query rewriter (for getting related defexpr), by planner (for + * acquiring an AccessShareLock on target variable), and by the executor + * (we need to know the target variable ID). + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * transformSetOperationStmt - * transforms a set-operations tree @@ -2021,6 +2267,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2496,6 +2743,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2563,6 +2811,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2750,9 +2999,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Transform the target reference. Typically we will get back a Param * node, but there's no reason to be too picky about its type. + * + * Session variables should not be used as target of a PL/pgSQL assign + * statement. So we should use a dedicated expression kind and disallow + * session variables there. The dedicated context allows to eliminate + * undesirable warnings about the possibility of a target PL/pgSQL variable + * shadowing a session variable. */ target = transformExpr(pstate, (Node *) cref, - EXPR_KIND_UPDATE_TARGET); + EXPR_KIND_ASSIGN_TARGET); targettype = exprType(target); targettypmod = exprTypmod(target); targetcollation = exprCollation(target); @@ -2794,6 +3049,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) */ type_id = exprType((Node *) tle->expr); + /* + * For indirection processing and additional casts we can use expr_kind + * EXPR_KIND_UPDATE_TARGET. + */ pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; if (indirection) @@ -2936,6 +3195,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ @@ -3209,6 +3470,14 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); + /* + * The arguments of CALL statement are evaluated by a direct expression + * executor call. This path is unsupported yet, so block it. It will be + * enabled later. + */ + if (pstate->p_hasSessionVariables) + elog(ERROR, "session variable cannot be used as an argument"); + assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 89fa23c2dd4..8ae368c513c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -293,7 +293,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -443,6 +443,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list + let_target %type <node> opt_routine_body %type <groupclause> group_clause @@ -734,7 +735,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1081,6 +1082,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12770,6 +12772,47 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET let_target '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = $2; + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + +let_target: + ColId opt_indirection + { + $$ = list_make1(makeString($1)); + if ($2) + $$ = list_concat($$, + check_indirection($2, yyscanner)); + } + ; + /***************************************************************************** * * QUERY: @@ -17840,6 +17883,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18451,6 +18495,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index efa730c1676..a7343001f21 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -580,6 +580,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -970,6 +975,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c2806297aa4..aba5259e027 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -17,6 +17,7 @@ #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -33,11 +34,13 @@ #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" /* GUC parameters */ @@ -106,6 +109,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* @@ -499,6 +505,89 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_POLICY: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_COPY_WHERE: + case EXPR_KIND_LET_TARGET: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + result = false; + break; + } + + return result; +} + /* * Transform a ColumnRef. * @@ -575,6 +664,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: /* okay */ break; @@ -847,8 +938,61 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) } /* - * Throw error if no translation found. + * There are contexts where session variables are not allowed. We don't + * need to identify session variables in such a context, but identifying + * them allows us to raise meaningful error messages like "you cannot use + * session variables here". */ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + { + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* ----- + * Session variables are shadowed by columns, routine variables or + * routine arguments. We certainly don't want to use a session variable + * when it is exactly shadowed, but a RTE like this is conceivable: + * + * CREATE TYPE t AS (c int); + * CREATE VARIABLE foo AS t; + * CREATE TABLE foo(a int, b int); + * + * SELECT foo.a, foo.b, foo.c FROM foo; + * + * However, that is very confusing, so we disallow it. We don't try to + * identify a variable if we know that it would be shadowed. + * ----- + */ + if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + { + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + node = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, cref->location); + } + } + } + + /* throw an error if no translation was found */ if (node == NULL) { switch (crerr) @@ -880,6 +1024,72 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable", attrname), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + static Node * transformParamRef(ParseState *pstate, ParamRef *pref) { @@ -1815,6 +2025,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_LET_TARGET: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -1858,6 +2069,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_ASSIGN_TARGET: + err = _("cannot use subquery as target of assign statement"); + break; /* * There is intentionally no default: case here, so that the @@ -3197,6 +3411,10 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_ASSIGN_TARGET: + return "ASSIGN"; + case EXPR_KIND_LET_TARGET: + return "LET"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9b23344a3b1..9aa4f60768b 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2656,6 +2656,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) err = _("set-returning functions are not allowed in column generation expressions"); break; case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: errkind = true; break; diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index 96f80b30463..dac78ba5cce 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 0c45fcf318f..85b809cb071 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -86,6 +86,9 @@ CreateQueryDesc(PlannedStmt *plannedstmt, qd->queryEnv = queryEnv; qd->instrument_options = instrument_options; /* instrumentation wanted? */ + qd->num_session_variables = 0; + qd->session_variables = NULL; + /* null these fields until set by ExecutorStart */ qd->tupDesc = NULL; qd->estate = NULL; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 217c6028497..69d2f7e1ced 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -49,6 +49,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -235,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1069,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2213,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2415,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3304,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a39068d1bf1..e7f74947cde 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -528,6 +529,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8617,6 +8619,14 @@ get_parameter(Param *param, deparse_context *context) return; } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Alternatively, maybe it's a subplan output, which we print as a * reference to the subplan. (We could drill down into the subplan and @@ -13399,6 +13409,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 5af1a168ec2..7e1e9e28f12 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -162,6 +163,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -1903,18 +1905,33 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, (void *) &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -1929,6 +1946,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already @@ -2060,7 +2091,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index e48a86be54b..eaae0c5a933 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 293648e30c7..ec572815c94 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1214,8 +1214,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4629,6 +4629,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 18ea6ac83a5..def691dafb3 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -171,7 +171,9 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror); extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 3810e040f28..db593cd5bed 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -35,6 +35,14 @@ CATALOG(pg_variable,9222,VariableRelationId) /* OID of entry in pg_type for variable's type */ Oid vartype BKI_LOOKUP(pg_type); + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + /* variable name */ NameData varname; diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..b3f03c65827 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,32 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "nodes/params.h" +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" +#include "tcop/cmdtag.h" +#include "utils/queryenvironment.h" + +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern void SetSessionVariableWithSecurityCheck(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull, Oid *typid); +extern Datum GetSessionVariableWithTypeCheck(Oid varid, bool *isNull, Oid expected_typid); + +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + +#endif diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h index 0a7274e26c8..d1b2f59e0cf 100644 --- a/src/include/executor/execdesc.h +++ b/src/include/executor/execdesc.h @@ -48,6 +48,10 @@ typedef struct QueryDesc EState *estate; /* executor's query-wide state */ PlanState *planstate; /* tree of per-plan-node state */ + /* reference to session variables buffer */ + int num_session_variables; + SessionVariableValue *session_variables; + /* This field is set by ExecutorRun */ bool already_executed; /* true if previously executed */ diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 182a6956bb0..b4464edc22a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -617,6 +617,18 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + Oid varid; + Oid typid; + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -669,6 +681,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0e08296b438..778db6ad4f2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -142,6 +142,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -162,6 +165,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -2100,6 +2105,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + int location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index add0f9e45fc..ea1f76d0fbb 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -163,6 +163,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -508,6 +511,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 52f29bcdb69..4549366cf59 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -49,7 +49,7 @@ typedef struct PlannedStmt NodeTag type; - CmdType commandType; /* select|insert|update|delete|merge|utility */ + CmdType commandType; /* select|let|insert|update|delete|merge|utility */ uint64 queryId; /* query identifier (copied from Query) */ @@ -94,6 +94,8 @@ typedef struct PlannedStmt Node *utilityStmt; /* non-null if this is utility stmt */ + List *sessionVariables; /* OIDs for PARAM_VARIABLE Params */ + /* statement location in source string (copied from Query) */ ParseLoc stmt_location; /* start location, or -1 if unknown */ ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index b0ef1952e8f..4b2424d6d34 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -361,6 +361,9 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramid holds the variable's OID). */ typedef enum ParamKind { @@ -368,6 +371,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -380,6 +384,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of session variable if it is used */ + Oid paramvarid; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 93137261e48..61341c50cf7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -124,4 +124,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 56670e65b11..aefe51f335b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -256,6 +256,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 2375e95c107..c11fdb3d970 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -82,6 +82,8 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ + EXPR_KIND_LET_TARGET, /* LET target */ } ParseExprKind; @@ -244,6 +246,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 9465df7b2ff..a921af2486f 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index a3d521b6f97..38cc87f4e7a 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 86c5bd324a9..1361a8a8480 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8099,7 +8099,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index d0aff56a01d..a2cb35e3d7d 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -56,3 +56,928 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; +-- check access rights +CREATE ROLE regress_noowner; +CREATE VARIABLE var1 AS int; +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; +LET var1 = 10; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +-- should fail +SET ROLE TO regress_noowner; +SELECT var1; +ERROR: permission denied for session variable var1 +SELECT sqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: SQL function "sqlfx" statement 1 +SELECT plpgsqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "$1 + var1" +PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +-- should be ok +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; +-- should be ok +SET ROLE TO regress_noowner; +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); +DROP ROLE regress_noowner; +-- use variables inside views +CREATE VARIABLE var1 AS numeric; +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- start a new session +\c +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- should fail, dependency +DROP VARIABLE var1; +ERROR: cannot drop session variable var1 because other objects depend on it +DETAIL: view test_view depends on session variable var1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view test_view +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; +LET var1 = 'str1'; +LET var2 = 'str2'; +SELECT sqlfx1(sqlfx2('Hello')); + sqlfx1 +------------------- + str1, str2, Hello +(1 row) + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + QUERY PLAN +------------------------------------------------------ + Result + Output: sqlfx1(sqlfx2('Hello'::character varying)) +(2 rows) + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; +set plan_cache_mode TO force_generic_plan; +LET var1 = 3.14; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 3.14 +(1 row) + +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 6.28 +(1 row) + +DROP VARIABLE var1; +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 9.42 +(1 row) + +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 12.56 +(1 row) + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); +set plan_cache_mode TO DEFAULT; +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; +NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} +DROP VARIABLE var1; +DROP VARIABLE var2; +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; +-- should not crash (but is not supported yet) +CALL p(v); +ERROR: session variable cannot be used as an argument +DO $$ BEGIN CALL p(v); END $$; +ERROR: session variable cannot be used as an argument +CONTEXT: SQL statement "CALL p(v)" +PL/pgSQL function inline_code_block line 1 at CALL +DROP PROCEDURE p(int); +DROP VARIABLE v; +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; +-- should fail +EXECUTE ptest(v); +ERROR: session variable cannot be used as an argument +DEALLOCATE ptest; +DROP VARIABLE v; +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; +-- should fail +LET var1 = pi(); +ERROR: session variable "var1" doesn't exist +LINE 1: LET var1 = pi(); + ^ +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + var1 +------------------ + 3.14159265358979 +(1 row) + +SET search_path TO svartest; +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + var1 +------------------ + 13.1415926535898 +(1 row) + +RESET search_path; +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to session variable svartest.var1 +CREATE VARIABLE var1 AS text; +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; +SELECT var1; + var1 +------- + hello +(1 row) + +DROP VARIABLE var1; +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; +-- should fail +SELECT var1; +ERROR: domain int_domain does not allow null values +-- should be ok +LET var1 = 1000; +SELECT var1; + var1 +------ + 1000 +(1 row) + +-- should fail +LET var1 = 10; +ERROR: value for domain int_domain violates check constraint "int_domain_check" +-- should fail +LET var1 = NULL; +ERROR: domain int_domain does not allow null values +-- note - domain defaults are not supported yet (like PLpgSQL) +DROP VARIABLE var1; +DROP DOMAIN int_domain; +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + var1 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +DROP VARIABLE var1; +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + var1 +------ + 100 +(1 row) + +SET search_path to public, svartest; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table foo +drop cascades to session variable var1 +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +LET var2[var1] = 0; +SELECT var2; + var2 +----------- + [2:2]={0} +(1 row) + +DROP VARIABLE var1, var2; +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +SELECT var2; + var2 +------ + {} +(1 row) + +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" +LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x; + ^ +-- should be ok +LET var1 = generate_series(1, 1); +-- should fail +LET var1 = generate_series(1, 2); +ERROR: expression returned more than one row +LET var1 = generate_series(1, 0); +ERROR: expression returned no rows +DROP VARIABLE var1; +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); +SELECT v1; + v1 +------------ + (1,2,3.00) +(1 row) + +SELECT v2; + v2 +--------------- + (10,20,31.40) +(1 row) + +SELECT (v1).*; + x | y | z +---+---+------ + 1 | 2 | 3.00 +(1 row) + +SELECT (v2).*; + x | y | z +----+----+------- + 10 | 20 | 31.40 +(1 row) + +SELECT v1.x + v1.z; + ?column? +---------- + 4.00 +(1 row) + +SELECT v2.x + v2.z; + ?column? +---------- + 41.40 +(1 row) + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should fail +SELECT v2.x; +ERROR: permission denied for session variable v2 +SET ROLE TO DEFAULT; +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; +CREATE TYPE t1 AS (a int, b numeric, c text); +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; + v1 +---------------------------- + (1,3.14159265358979,hello) +(1 row) + +LET v1.b = 10.2222; +SELECT v1; + v1 +------------------- + (1,10.2222,hello) +(1 row) + +-- should fail, attribute doesn't exist +LET v1.x = 10; +ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +LINE 1: LET v1.x = 10; + ^ +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; +ERROR: assignment expression returned 3 columns +LINE 1: LET v1 = (NULL::t1).*; + ^ +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + v1 +------------- + (1,10.2222) +(1 row) + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + v1 +-------------- + (1,10.2222,) +(1 row) + +LET v1 = (10, 10.3, 20); +SELECT v1; + v1 +-------------- + (10,10.3,20) +(1 row) + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + v1 +--------- + (10,20) +(1 row) + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; +ERROR: cannot alter type "t1" because session variable "public.v1" uses it +DROP VARIABLE v1; +DROP TYPE t1; +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + var1 +---------------------------------- + (10,3.14159265358979,05-26-2023) +(1 row) + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; +ERROR: cannot alter table "svar_test" because session variable "public.var1" uses it +-- should fail +DROP TABLE svar_test; +ERROR: cannot drop table svar_test because other objects depend on it +DETAIL: session variable var1 depends on type svar_test +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VARIABLE var1; +DROP TABLE svar_test; +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + var1 +------------ + {10.1,2.1} +(1 row) + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; +ERROR: set-returning functions are not allowed in LET +LINE 1: LET var1[generate_series(1,3)] = 100; + ^ +DROP VARIABLE var1; +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; + var1 +-------------------- + (10.2,"{0.0,0.0}") +(1 row) + +LET var1.b[1] = 10.3; +SELECT var1; + var1 +--------------------- + (10.2,"{10.3,0.0}") +(1 row) + +DROP VARIABLE var1; +DROP TYPE t1; +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + count +------- + 100 +(1 row) + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + QUERY PLAN +----------------------------------- + Aggregate + -> Seq Scan on svar_test + Filter: ((a % 10) = zero) +(3 rows) + +LET zero = (SELECT count(*) FROM svar_test); +-- result should be 1000 +SELECT zero; + zero +------ + 1000 +(1 row) + +DROP VARIABLE zero; +DROP TABLE svar_test; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO on; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO off; +DROP VIEW var1view; +DROP VARIABLE var1; +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; + id | v +----+----- + 1 | 100 +(1 row) + +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+----- + 1 | 200 +(1 row) + +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+--- +(0 rows) + +DROP TABLE svar_test; +DROP VARIABLE varid; +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + relname +---------- + pg_class +(1 row) + +DROP VARIABLE var1; +CREATE TABLE xxtab(avar int); +INSERT INTO xxtab VALUES(333); +CREATE TYPE xxtype AS (avar int); +CREATE VARIABLE xxtab AS xxtype; +INSERT INTO xxtab VALUES(10); +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +CREATE VARIABLE public.avar AS int; +-- should be ok, see the table +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT public.avar FROM xxtab; + avar +------ + + +(2 rows) + +DROP VARIABLE xxtab; +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +DROP VARIABLE public.avar; +DROP TYPE xxtype; +DROP TABLE xxtab; +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; +CREATE TABLE public.svar(a int, b int); +INSERT INTO public.svar VALUES(10, 20); +LET public.svar = (100, 200, 300); +-- should be ok +-- show table +SELECT * FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; +CREATE TYPE ab AS (a integer, b integer); +CREATE VARIABLE v_ab AS ab; +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +CREATE SCHEMA v_ab; +CREATE VARIABLE v_ab.a AS integer; +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; +SET search_path TO public; +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); +-- should be ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; +-- should be still ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; +ERROR: session variable reference "xxx_am.b" is ambiguous +LINE 1: SELECT xxx_am.b; + ^ +-- enhanced references should be ok +SELECT public.xxx_am.b; + b +---- + 10 +(1 row) + +SELECT :"DBNAME".xxx_am.b; + b +---- + 20 +(1 row) + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); +-- we should see table +SELECT xxx_am.b FROM xxx_am; + b +---- + 10 +(1 row) + +SELECT x.b FROM xxx_am x; + b +---- + 10 +(1 row) + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; +CREATE SCHEMA :"DBNAME"; +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; +SET search_path TO :"DBNAME"; +-- should be ambiguous +SELECT :"DBNAME".b; +ERROR: session variable reference "regression.b" is ambiguous +LINE 1: SELECT "regression".b; + ^ +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; +ERROR: session variable reference "regression.regression.b" is ambiguous +LINE 1: SELECT "regression"."regression".b; + ^ +CREATE TABLE :"DBNAME"(b int); +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + b +--- +(0 rows) + +DROP TABLE :"DBNAME"; +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; +RESET search_path; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index 82679cbf48e..6445479d22d 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -59,3 +59,642 @@ DROP VARIABLE svartest.var1; DROP SCHEMA svartest; DROP ROLE regress_variable_reader; DROP ROLE regress_variable_owner; + +-- check access rights +CREATE ROLE regress_noowner; + +CREATE VARIABLE var1 AS int; + +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; + +LET var1 = 10; +-- should be ok +SELECT var1; +SELECT sqlfx(20); +SELECT sqlfx_sd(20); +SELECT plpgsqlfx(20); +SELECT plpgsqlfx_sd(20); + +-- should fail +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +-- should be ok +SELECT sqlfx_sd(20); +SELECT plpgsqlfx_sd(20); + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; + +-- should be ok +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); + +DROP ROLE regress_noowner; + +-- use variables inside views +CREATE VARIABLE var1 AS numeric; + +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- start a new session +\c + +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- should fail, dependency +DROP VARIABLE var1; + +-- should be ok +DROP VARIABLE var1 CASCADE; + +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; + +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; + +LET var1 = 'str1'; +LET var2 = 'str2'; + +SELECT sqlfx1(sqlfx2('Hello')); + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; + +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; + +set plan_cache_mode TO force_generic_plan; + +LET var1 = 3.14; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; + +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; + +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + +DROP VARIABLE var1; +DROP FUNCTION plpgsqlfx(); + +set plan_cache_mode TO DEFAULT; + +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; + +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; + +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; + +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; + +-- should not crash (but is not supported yet) +CALL p(v); + +DO $$ BEGIN CALL p(v); END $$; + +DROP PROCEDURE p(int); +DROP VARIABLE v; + +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; + +-- should fail +EXECUTE ptest(v); + +DEALLOCATE ptest; +DROP VARIABLE v; + +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; + +-- should fail +LET var1 = pi(); +SELECT var1; + +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + +SET search_path TO svartest; + +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + +RESET search_path; +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS text; + +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; + +SELECT var1; + +DROP VARIABLE var1; + +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; + +-- should fail +SELECT var1; + +-- should be ok +LET var1 = 1000; +SELECT var1; + +-- should fail +LET var1 = 10; + +-- should fail +LET var1 = NULL; + +-- note - domain defaults are not supported yet (like PLpgSQL) + +DROP VARIABLE var1; +DROP DOMAIN int_domain; + +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; + +DROP VARIABLE var1; + +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + +SET search_path to public, svartest; + +SELECT var1; + +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +LET var2[var1] = 0; + +SELECT var2; + +DROP VARIABLE var1, var2; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +SELECT var2; + +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; + +-- should be ok +LET var1 = generate_series(1, 1); + +-- should fail +LET var1 = generate_series(1, 2); +LET var1 = generate_series(1, 0); + +DROP VARIABLE var1; + +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); + +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; + +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); + +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); + +SELECT v1; +SELECT v2; +SELECT (v1).*; +SELECT (v2).*; + +SELECT v1.x + v1.z; +SELECT v2.x + v2.z; + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; + +SET ROLE TO regress_var_test_role; + +-- should fail +SELECT v2.x; + +SET ROLE TO DEFAULT; + +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; + +CREATE TYPE t1 AS (a int, b numeric, c text); + +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; +LET v1.b = 10.2222; +SELECT v1; + +-- should fail, attribute doesn't exist +LET v1.x = 10; + +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; + +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + +LET v1 = (10, 10.3, 20); +SELECT v1; + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; + +DROP VARIABLE v1; +DROP TYPE t1; + +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; + +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; + +-- should fail +DROP TABLE svar_test; + +DROP VARIABLE var1; +DROP TABLE svar_test; + +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; + +DROP VARIABLE var1; + +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; +LET var1.b[1] = 10.3; +SELECT var1; + +DROP VARIABLE var1; +DROP TYPE t1; + +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; + +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + +LET zero = (SELECT count(*) FROM svar_test); + +-- result should be 1000 +SELECT zero; + +DROP VARIABLE zero; +DROP TABLE svar_test; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; + +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; + +SELECT * FROM var1view; + +SET debug_parallel_query TO on; + +SELECT * FROM var1view; + +SET debug_parallel_query TO off; + +DROP VIEW var1view; +DROP VARIABLE var1; + +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); + +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + +DROP TABLE svar_test; +DROP VARIABLE varid; + + +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + +DROP VARIABLE var1; + +CREATE TABLE xxtab(avar int); + +INSERT INTO xxtab VALUES(333); + +CREATE TYPE xxtype AS (avar int); + +CREATE VARIABLE xxtab AS xxtype; + +INSERT INTO xxtab VALUES(10); + +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + +-- should be ok +SELECT avar FROM xxtab; + +CREATE VARIABLE public.avar AS int; + +-- should be ok, see the table +SELECT avar FROM xxtab; + +-- should be ok +SELECT public.avar FROM xxtab; + +DROP VARIABLE xxtab; + +SELECT xxtab.avar FROM xxtab; + +DROP VARIABLE public.avar; + +DROP TYPE xxtype; + +DROP TABLE xxtab; + +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; + +CREATE TABLE public.svar(a int, b int); + +INSERT INTO public.svar VALUES(10, 20); + +LET public.svar = (100, 200, 300); + +-- should be ok +-- show table +SELECT * FROM public.svar; +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; + +CREATE TYPE ab AS (a integer, b integer); + +CREATE VARIABLE v_ab AS ab; + +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); + +-- we should see table +SELECT v_ab.a FROM v_ab; + +CREATE SCHEMA v_ab; + +CREATE VARIABLE v_ab.a AS integer; + +-- we should see table +SELECT v_ab.a FROM v_ab; + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; + +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; + +SET search_path TO public; + +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); + +-- should be ok +SELECT xxx_am; + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; + +-- should be still ok +SELECT xxx_am; + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; + +-- enhanced references should be ok +SELECT public.xxx_am.b; +SELECT :"DBNAME".xxx_am.b; + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); + +-- we should see table +SELECT xxx_am.b FROM xxx_am; +SELECT x.b FROM xxx_am x; + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; + +CREATE SCHEMA :"DBNAME"; + +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; + +SET search_path TO :"DBNAME"; + +-- should be ambiguous +SELECT :"DBNAME".b; + +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; + +CREATE TABLE :"DBNAME"(b int); + +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + +DROP TABLE :"DBNAME"; + +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; + +RESET search_path; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9e4f3c1444d..68e3c3c7b63 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1498,6 +1498,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey @@ -2603,6 +2604,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData @@ -2793,6 +2795,9 @@ SupportRequestRows SupportRequestSelectivity SupportRequestSimplify SupportRequestWFuncMonotonic +SVariable +SVariableData +SVariableState Syn SyncOps SyncRepConfigData -- 2.47.0 [text/x-patch] v20241120-0001-Enhancing-catalog-for-support-session-variables-and-.patch (129.9K, 23-v20241120-0001-Enhancing-catalog-for-support-session-variables-and-.patch) download | inline diff: From b453769c90f0c8992b303ab9117c2437414f816b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 15:41:55 +0100 Subject: [PATCH 01/21] Enhancing catalog for support session variables and related support This patch introduces new system catalog table pg_variable. This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. Both commands are implemented by this patch. Possibility to change owner, schema or rename. Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. This patch enhancing pg_dump and psql to support session variables. The changes are related to system catalog. This patch is not short, but the code is simple. --- doc/src/sgml/catalogs.sgml | 120 +++++++++ doc/src/sgml/ddl.sgml | 31 +++ doc/src/sgml/func.sgml | 13 + doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/ref/allfiles.sgml | 3 + .../sgml/ref/alter_default_privileges.sgml | 26 +- doc/src/sgml/ref/alter_variable.sgml | 178 ++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 7 +- doc/src/sgml/ref/create_variable.sgml | 151 +++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++ doc/src/sgml/ref/grant.sgml | 6 + doc/src/sgml/ref/revoke.sgml | 7 + doc/src/sgml/reference.sgml | 3 + src/backend/catalog/Makefile | 1 + src/backend/catalog/aclchk.c | 78 ++++++ src/backend/catalog/dependency.c | 6 + src/backend/catalog/meson.build | 1 + src/backend/catalog/namespace.c | 133 +++++++++ src/backend/catalog/objectaddress.c | 121 ++++++++- src/backend/catalog/pg_shdepend.c | 2 + src/backend/catalog/pg_variable.c | 255 ++++++++++++++++++ src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/seclabel.c | 1 + src/backend/commands/tablecmds.c | 41 +++ src/backend/parser/gram.y | 105 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 16 ++ src/backend/utils/adt/acl.c | 7 + src/backend/utils/cache/lsyscache.c | 113 ++++++++ src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 + src/bin/pg_dump/pg_dump.c | 195 +++++++++++++- src/bin/pg_dump/pg_dump.h | 19 ++ src/bin/pg_dump/pg_dump_sort.c | 6 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 96 +++++++ src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 42 ++- src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/namespace.h | 4 + src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 3 + src/include/catalog/pg_variable.h | 81 ++++++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 2 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/acl.h | 1 + src/include/utils/lsyscache.h | 9 + src/test/regress/expected/dependency.out | 17 ++ src/test/regress/expected/oidjoins.out | 4 + src/test/regress/expected/psql.out | 50 ++++ .../regress/expected/session_variables.out | 58 ++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 + src/test/regress/sql/psql.sql | 21 ++ src/test/regress/sql/session_variables.sql | 61 +++++ src/tools/pgindent/typedefs.list | 4 + 65 files changed, 2365 insertions(+), 26 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h create mode 100644 src/test/regress/expected/session_variables.out create mode 100644 src/test/regress/sql/session_variables.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 59bb833f48d..bdd2ca0152f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9734,4 +9739,119 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 08155b156a5..2e3f0b95fb0 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5298,6 +5298,37 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. + </para> + + <para> + Inside a query or an expression, a session variable can be + <quote>shadowed</quote> by a column with the same name. Similarly, the + name of a function or procedure argument or a PL/pgSQL variable (see + <xref linkend="plpgsql-declarations"/>) can shadow a session variable + in the routine's body. Such collisions of identifiers can be resolved + by using qualified identifiers: Session variables can be qualified with + the schema name, columns can use table aliases, routine variables can use + block labels, and routine arguments can use the routine name. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 72f223a0414..6163abf2ff7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25736,6 +25736,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index f54f25c1c6b..d0cb53763aa 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1664,6 +1664,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 89aacec4fab..041c78d432f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -51,6 +51,10 @@ GRANT { { USAGE | CREATE } ON SCHEMAS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -83,6 +87,12 @@ REVOKE [ GRANT OPTION FOR ] ON SCHEMAS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -117,14 +127,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, and types (including domains) can be - altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), and session + variables can be altered. For this command, functions include aggregates + and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard + term for functions and procedures taken together. In earlier PostgreSQL + releases, only the word <literal>FUNCTIONS</literal> was allowed. It is not + possible to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..d87570e7d8b --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..a834c876bc9 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..ec165ab96fc --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,151 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> ] [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..78ff10fcf5e 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..626c0231d0d 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -137,6 +137,13 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] | CURRENT_ROLE | CURRENT_USER | SESSION_USER + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 1589a75fd53..7a90fcfc457 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index aaf96a965e4..3620eb356c6 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,19 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach(cell, objnames) + { + RangeVar *varvar = (RangeVar *) lfirst(cell); + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +870,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1005,6 +1055,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_SCHEMA; errormsg = gettext_noop("invalid privilege type %s for schema"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1196,6 +1250,12 @@ SetDefaultACL(InternalDefaultACL *iacls) this_privileges = ACL_ALL_RIGHTS_SCHEMA; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; + default: elog(ERROR, "unrecognized object type: %d", (int) iacls->objtype); @@ -1439,6 +1499,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_NAMESPACE: iacls.objtype = OBJECT_SCHEMA; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1499,6 +1562,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2732,6 +2798,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2843,6 +2912,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("must be owner of type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("must be owner of view %s"); break; @@ -2991,6 +3063,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4258,6 +4332,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_NAMESPACE; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 0489cbabcb8..c9d686665de 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 2f3ded8a0e7..bfdb4da3301 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 30807f91904..86e2258657d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -986,6 +987,67 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + ListCell *l; + + visible = false; + foreach(l, activeSearchPath) + { + Oid namespaceId = lfirst_oid(l); + + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3351,66 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid namespaceId; + Oid varoid = InvalidOid; + ListCell *l; + + if (nspname) + { + namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach(l, activeSearchPath) + { + namespaceId = lfirst_oid(l); + + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} /* * DeconstructQualifiedName @@ -5085,3 +5207,14 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + + if (!SearchSysCacheExists1(VARIABLEOID, ObjectIdGetDatum(oid))) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(VariableIsVisible(oid)); +} diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 7520a982151..2c8b9d7fd3c 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2005,16 +2027,20 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_NAMESPACE: objtype_str = "schemas"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, - DEFACLOBJ_NAMESPACE))); + DEFACLOBJ_NAMESPACE, + DEFACLOBJ_VARIABLE))); } /* @@ -2098,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2292,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2463,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3451,6 +3497,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -3803,6 +3875,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new schemas belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -4619,6 +4701,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -5725,6 +5811,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on schemas"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) @@ -5965,6 +6055,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, varname); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 753afb88453..1ea3d6db05c 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..e3a861c8cba --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,255 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +static ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + + +/* + * Creates entry in pg_variable table + */ +static ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + Acl *varacl; + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation;; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + /* we want SessionVariableCreatePostprocess to see the catalog changes. */ + CommandCounterIncrement(); + + return variable; +} + +/* + * Drop variable by OID, and register the needed session variable + * cleanup. + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index a45f3bb6b83..d4d88d11314 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -139,6 +140,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -418,6 +423,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -558,6 +564,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -640,6 +647,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -870,6 +878,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index 85eec7e3947..3cce17e92af 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index dcfc1dbaffd..d03cb947e67 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 5607273bf9f..c026ab5dcb8 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index c632d1e8245..3deef69295d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6768,6 +6769,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6831,6 +6833,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 67eb96396af..89fa23c2dd4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -281,8 +282,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -775,8 +776,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES + VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1042,6 +1043,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1584,6 +1586,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5210,6 +5213,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -6998,6 +7029,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -7874,6 +7906,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -7919,6 +7959,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8116,6 +8164,7 @@ defacl_privilege_target: | SEQUENCES { $$ = OBJECT_SEQUENCE; } | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -9904,6 +9953,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10265,6 +10332,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10546,6 +10631,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -17917,6 +18010,8 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18570,6 +18665,8 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 0f324ee4e31..4eaed1f8439 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4039,6 +4040,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4107,6 +4109,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4120,6 +4131,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f28bf371059..217c6028497 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -23,6 +23,7 @@ #include "catalog/namespace.h" #include "catalog/pg_authid.h" #include "catalog/pg_inherits.h" +#include "catalog/pg_variable.h" #include "catalog/toasting.h" #include "commands/alter.h" #include "commands/async.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 2a716cc6b7f..98522a45612 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -851,6 +851,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +952,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index a85dc0d891f..b464a7f090a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -38,6 +38,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3714,3 +3715,115 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} + +/* + * Returns the type of the given session variable. + */ +Oid +get_session_variable_type(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid vartype; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + vartype = varform->vartype; + + ReleaseSysCache(tup); + + return vartype; +} + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 33d323085f5..b0451c1b903 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 5649859aa1e..50de5d0ea29 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -511,6 +511,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index 68ae2970ade..3fa61090c64 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 8c20c263c4b..6f012a1b558 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3098,6 +3098,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3647,6 +3655,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index a8c141b689d..b8d839ece41 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -363,6 +363,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5416,6 +5417,190 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + resetPQExpBuffer(query); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || dopt->dataOnly) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -10140,7 +10325,8 @@ getAdditionalACLs(Archive *fout) dobj->objType == DO_TABLE || dobj->objType == DO_PROCLANG || dobj->objType == DO_FDW || - dobj->objType == DO_FOREIGN_SERVER) + dobj->objType == DO_FOREIGN_SERVER || + dobj->objType == DO_VARIABLE) { DumpableObjectWithAcl *daobj = (DumpableObjectWithAcl *) dobj; @@ -10739,6 +10925,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_SUBSCRIPTION_REL: dumpSubscriptionTable(fout, (const SubRelInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -15188,6 +15377,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_NAMESPACE: type = "SCHEMAS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -18898,6 +19090,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index d65f5585651..f2f558b2961 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -52,6 +52,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -699,6 +700,23 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + char *varacl; + char *rvaracl; + char *initvaracl; + char *initrvaracl; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -782,5 +800,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 3c8f2eb808d..ca323171aa8 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -118,6 +119,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1500,6 +1502,10 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "POST-DATA BOUNDARY (ID %d)", obj->dumpId); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index aa1564cd450..dd8c054a6a6 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -789,6 +789,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1674,6 +1684,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -3942,6 +3969,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4404,6 +4449,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 1f3cbb11f7c..7594f1010a6 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1077,6 +1077,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 5bfebad64d5..c52c8fd147f 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -5195,6 +5195,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 273f974538e..4e15a180ea6 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 02fe5d151e0..317bc2eab6e 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -265,6 +265,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[S+] [PATTERN] list data types\n"); HELP0(" \\du[S+] [PATTERN] list roles\n"); HELP0(" \\dv[S+] [PATTERN] list views\n"); + HELP0(" \\dV [PATTERN] list session variables\n"); HELP0(" \\dx[+] [PATTERN] list extensions\n"); HELP0(" \\dX [PATTERN] list extended statistics\n"); HELP0(" \\dy[+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fad2277991d..293648e30c7 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1304,6 +1311,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1865,7 +1873,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\errverbose", "\\ev", "\\f", @@ -2556,6 +2564,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3121,7 +3132,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3931,6 +3942,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4193,6 +4211,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4394,7 +4418,7 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4416,7 +4440,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4424,7 +4449,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4460,6 +4486,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4762,7 +4790,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5228,6 +5256,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 167f91a6e3f..507f3808218 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index f70d1daba52..6eec79d2eec 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8d434d48d57..18ea6ac83a5 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,8 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index d272cdf08b4..529d53c6bb6 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -68,6 +68,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_FUNCTION 'f' /* function */ #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index cbbe8acd382..41f1121d19f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6566,6 +6566,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9221', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..3810e040f28 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,81 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +extern ObjectAddress CreateVariable(ParseState *pstate, + CreateSessionVarStmt *stmt); +extern void DropVariableById(Oid varid); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +#endif /* PG_VARIABLE_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 0f9462493e3..0e08296b438 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2316,6 +2316,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3447,6 +3448,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 899d64ad55f..56670e65b11 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -484,6 +484,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 7fdcec6dd93..9465df7b2ff 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 731d84b2a93..98aab98229e 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 20446f6f836..8a3684e711f 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -134,6 +134,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -206,6 +207,14 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); +extern Oid get_session_variable_type(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 74d9ff2998d..c336f67a596 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 36dc31c16c4..88e21194711 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5930,6 +5930,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6151,6 +6175,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6385,6 +6415,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6554,6 +6590,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of relations @@ -6687,6 +6729,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6734,6 +6782,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out new file mode 100644 index 00000000000..d0aff56a01d --- /dev/null +++ b/src/test/regress/expected/session_variables.out @@ -0,0 +1,58 @@ +CREATE ROLE regress_variable_owner; +-- should be ok +CREATE VARIABLE var1 AS int; +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; +ERROR: session variable cannot be pseudo-type anyelement +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; +NOTICE: session variable "var2" does not exist, skipping +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; +NOTICE: session variable "var1" already exists, skipping +-- should fail +CREATE VARIABLE var1 AS int; +ERROR: session variable "var1" already exists +-- should be ok +DROP VARIABLE IF EXISTS var1; +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + obj_description +----------------------- + some variable comment +(1 row) + +DROP VARIABLE var1; +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +CREATE ROLE regress_variable_reader; +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; +DROP VARIABLE public.varxx; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; +\dV+ svartest.var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +----------+------+---------+-----------+------------------------+--------------------------------------------------+------------- + svartest | var1 | integer | | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| + | | | | | regress_variable_reader=r/regress_variable_owner | +(1 row) + +DROP VARIABLE svartest.var1; +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 81e4222d26a..05eb784f62b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -111,7 +111,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does a reconnect which transiently uses 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index c5021fc0b13..666eddb632d 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1631,6 +1631,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1742,6 +1755,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1783,6 +1799,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1823,6 +1841,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1847,6 +1866,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1872,6 +1892,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql new file mode 100644 index 00000000000..82679cbf48e --- /dev/null +++ b/src/test/regress/sql/session_variables.sql @@ -0,0 +1,61 @@ +CREATE ROLE regress_variable_owner; + +-- should be ok +CREATE VARIABLE var1 AS int; + +-- should fail, pseudotypes are not allowed +CREATE VARIABLE var2 AS anyelement; + +-- should be ok, do nothing +DROP VARIABLE IF EXISTS var2; + +-- do nothing +CREATE VARIABLE IF NOT EXISTS var1 AS int; + +-- should fail +CREATE VARIABLE var1 AS int; + +-- should be ok +DROP VARIABLE IF EXISTS var1; + +-- check comment on variable +CREATE VARIABLE var1 AS int; +COMMENT ON VARIABLE var1 IS 'some variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'var1'; + +DROP VARIABLE var1; + +--- check access rights and supported ALTER +CREATE SCHEMA svartest; +GRANT ALL ON SCHEMA svartest TO regress_variable_owner; + +CREATE VARIABLE svartest.var1 AS int; + +CREATE ROLE regress_variable_reader; + +GRANT SELECT ON VARIABLE svartest.var1 TO regress_variable_reader; +REVOKE ALL ON VARIABLE svartest.var1 FROM regress_variable_reader; + +ALTER VARIABLE svartest.var1 OWNER TO regress_variable_owner; +ALTER VARIABLE svartest.var1 RENAME TO varxx; +ALTER VARIABLE svartest.varxx SET SCHEMA public; + +DROP VARIABLE public.varxx; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner + IN SCHEMA svartest + GRANT SELECT ON VARIABLES TO regress_variable_reader; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner; +CREATE VARIABLE svartest.var1 AS int; +SET ROLE TO DEFAULT; + +\dV+ svartest.var1 + +DROP VARIABLE svartest.var1; + +DROP SCHEMA svartest; +DROP ROLE regress_variable_reader; +DROP ROLE regress_variable_owner; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 08521d51a9b..9e4f3c1444d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -535,6 +535,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext @@ -874,6 +875,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -933,6 +935,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData @@ -3079,6 +3082,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.47.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-05-21 07:12 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 1 sibling, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-05-21 07:12 UTC (permalink / raw) To: Dmitry Dolgov <[email protected]>; +Cc: Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; Bruce Momjian <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]> Hi út 19. 11. 2024 v 20:14 odesílatel Pavel Stehule <[email protected]> napsal: > Hi > > I wrote POC of VARIABLE(varname) syntax support > > here is a copy from regress test > > SET session_variables_ambiguity_warning TO on; > SET session_variables_use_fence_warning_guard TO on; > SET search_path TO 'public'; > CREATE VARIABLE a AS int; > LET a = 10; > CREATE TABLE test_table(a int, b int); > INSERT INTO test_table VALUES(20, 20); > -- no warning > SELECT a; > a.. > ---- > 10 > (1 row) > > -- warning variable is shadowed > SELECT a, b FROM test_table; > WARNING: session variable "a" is shadowed > LINE 1: SELECT a, b FROM test_table; > ^ > DETAIL: Session variables can be shadowed by columns, routine's variables > and routine's arguments with the same name. > a | b.. > ----+---- > 20 | 20 > (1 row) > > -- no warning > SELECT variable(a) FROM test_table; > a.. > ---- > 10 > (1 row) > > ALTER TABLE test_table DROP COLUMN a; > -- warning - variable fence is not used > SELECT a, b FROM test_table; > WARNING: session variable "a" is not used inside variable fence > LINE 1: SELECT a, b FROM test_table; > ^ > DETAIL: The collision of session variable' names and column names is > possible. > a | b.. > ----+---- > 10 | 20 > (1 row) > > -- no warning > SELECT variable(a), b FROM test_table; > a | b.. > ----+---- > 10 | 20 > (1 row) > > DROP VARIABLE a; > DROP TABLE test_table; > SET session_variables_ambiguity_warning TO DEFAULT; > SET session_variables_use_fence_warning_guard TO DEFAULT; > SET search_path TO DEFAULT; > > Last discussion is related to reducing the size of the session variable patch set. I have an idea to use variable's fencing more aggressively from the start, and then we can reduce it in future. This should not break issues with compatibility and doesn't need some like version flags. The real problem of proposed session variables is possible collisions between session variables identifiers and table or columns identifiers. I designed some tools to minimize the risk of unwanted collisions, but these tools increase the size of code and don't reduce the complexity of the patch and tests. The proposed change probably doesn't reduce a lot of code, but can reduce some tests, and mainly possible risk of some unwanted impact - at the end it can be less work for reviewers and less stress for committers - and the implementation can be divided to allone workable following steps. Step 1 ===== So the main change is the hard requirement for usage variable's fence everywhere where collisions are possible - and then in the first step, the collisions will not be possible, and then we don't need it to solve, and we don't need to test it. CREATE VARIABLE public.foo AS int; LET foo = 10; SELECT VARIABLE(foo); DO $$ BEGIN RAISE NOTICE '% %', VARIABLE(foo), VARIABLE(public.foo); END; $$; Step 2 ===== Necessity of usage variable fencing in PL/pgSQL can be a problem for migration from PL/SQL. But this can be solved separately by using SPI params hooks - similar to how PL/pgSQL works with PL/pgSQL variables. In this step we can push optimization for fast execution of the LET statement or optimization of usage variables in queries. After this step will be possible: DO $$ BEGIN RAISE NOTICE '% %', foo, VARIABLE(public.foo); END; $$; SELECT VARIABLE(foo); No other visible change in this step. WIth this step the people who do migration form Oracle and PL/pgSQL developers will be very happy. They don't need more. There can be collisions, but the collisions can be limited just to PL/pgSQL scope, and we can use already implemented mechanisms. Step 3 ===== We can talk in future about less requirement of usage variable fencing in queries. This needs to introduce some form of detection collisions and how they should be solved (outside PL/pgSQL). We can talk about other features like temporal, default values, transactional, etc ... This proposal doesn't reduce lines of code, but significantly reduces possible impacts of introducing session variables to other parts of SQL. Moreover, it allows us to separate some work and related discussion into separate blocks - any block can be implemented in different major pg releases. I think a lot of users will be very happy just with step 1 and step 2, and anything else can be discussed in future. Is this plan acceptable? Regards Pavel > Regards > > Pavel > ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-05-21 21:22 Bruce Momjian <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Bruce Momjian @ 2025-05-21 21:22 UTC (permalink / raw) To: Pavel Stehule <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]> On Wed, May 21, 2025 at 09:12:54AM +0200, Pavel Stehule wrote: > Last discussion is related to reducing the size of the session variable patch > set. > > I have an idea to use variable's fencing more aggressively from the start, and > then we can reduce it in future. This should not break issues with > compatibility and doesn't need some like version flags. > > The real problem of proposed session variables is possible collisions between > session variables identifiers and table or columns identifiers. I designed some > tools to minimize the risk of unwanted collisions, but these tools increase the > size of code and don't reduce the complexity of the patch and tests. The > proposed change probably doesn't reduce a lot of code, but can reduce some > tests, and mainly possible risk of some unwanted impact - at the end it can be > less work for reviewers and less stress for committers - and the implementation > can be divided to allone workable following steps. Yes, I remember the discussions about how the creation of server variables could break existing queries. Our scoping rules are already complex, so adding another scope would add a lot of complexity. > Step 1 > ===== > > So the main change is the hard requirement for usage variable's fence > everywhere where collisions are possible - and then in the first step, the > collisions will not be possible, and then we don't need it to solve, and we > don't need to test it. > > CREATE VARIABLE public.foo AS int; > LET foo = 10; > SELECT VARIABLE(foo); Yes, I can see how adding fencing like VARIABLE() would simplify things. > Step 2 > ===== > Necessity of usage variable fencing in PL/pgSQL can be a problem for migration > from PL/SQL. But this can be solved separately by using SPI params hooks - > similar to how PL/pgSQL works with PL/pgSQL variables. In this step we can push > optimization for fast execution of the LET statement or optimization of usage > variables in queries. Yes, there is already going to be migration requirements in moving from PL/SQL to PL/pgSQL, so the requirement to add VARIABLE() seems minimal. > After this step will be possible: > > DO $$ > BEGIN > RAISE NOTICE '% %', foo, VARIABLE(public.foo); > END; > $$; > > SELECT VARIABLE(foo); > > No other visible change in this step. WIth this step the people who do > migration form Oracle and PL/pgSQL developers will be very happy. They don't > need more. There can be collisions, but the collisions can be limited just to > PL/pgSQL scope, and we can use already implemented mechanisms. > > Step 3 > ===== > We can talk in future about less requirement of usage variable fencing in > queries. This needs to introduce some form of detection collisions and how they > should be solved (outside PL/pgSQL). > We can talk about other features like temporal, default values, transactional, > etc ... I feel that if we haven't found a good solution to this in 13 years, we should assume it is unsolvable and just accept an imperfect solution. -- Bruce Momjian <[email protected]> https://momjian.us EDB https://enterprisedb.com Do not let urgent matters crowd out time for investment in the future. ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-03 07:26 Pavel Stehule <[email protected]> parent: Bruce Momjian <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-03 07:26 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi st 21. 5. 2025 v 23:22 odesílatel Bruce Momjian <[email protected]> napsal: > On Wed, May 21, 2025 at 09:12:54AM +0200, Pavel Stehule wrote: > > Last discussion is related to reducing the size of the session variable > patch > > set. > > > > I have an idea to use variable's fencing more aggressively from the > start, and > > then we can reduce it in future. This should not break issues with > > compatibility and doesn't need some like version flags. > > > > The real problem of proposed session variables is possible collisions > between > > session variables identifiers and table or columns identifiers. I > designed some > > tools to minimize the risk of unwanted collisions, but these tools > increase the > > size of code and don't reduce the complexity of the patch and tests. The > > proposed change probably doesn't reduce a lot of code, but can reduce > some > > tests, and mainly possible risk of some unwanted impact - at the end it > can be > > less work for reviewers and less stress for committers - and the > implementation > > can be divided to allone workable following steps. > > Yes, I remember the discussions about how the creation of server > variables could break existing queries. Our scoping rules are already > complex, so adding another scope would add a lot of complexity. > > > Step 1 > > ===== > > > > So the main change is the hard requirement for usage variable's fence > > everywhere where collisions are possible - and then in the first step, > the > > collisions will not be possible, and then we don't need it to solve, and > we > > don't need to test it. > > > > CREATE VARIABLE public.foo AS int; > > LET foo = 10; > > SELECT VARIABLE(foo); > > Yes, I can see how adding fencing like VARIABLE() would simplify things. > > > Step 2 > > ===== > > Necessity of usage variable fencing in PL/pgSQL can be a problem for > migration > > from PL/SQL. But this can be solved separately by using SPI params hooks > - > > similar to how PL/pgSQL works with PL/pgSQL variables. In this step we > can push > > optimization for fast execution of the LET statement or optimization of > usage > > variables in queries. > > Yes, there is already going to be migration requirements in moving from > PL/SQL to PL/pgSQL, so the requirement to add VARIABLE() seems minimal. > > > After this step will be possible: > > > > DO $$ > > BEGIN > > RAISE NOTICE '% %', foo, VARIABLE(public.foo); > > END; > > $$; > > > > SELECT VARIABLE(foo); > > > > No other visible change in this step. WIth this step the people who do > > migration form Oracle and PL/pgSQL developers will be very happy. They > don't > > need more. There can be collisions, but the collisions can be limited > just to > > PL/pgSQL scope, and we can use already implemented mechanisms. > > > > Step 3 > > ===== > > We can talk in future about less requirement of usage variable fencing in > > queries. This needs to introduce some form of detection collisions and > how they > > should be solved (outside PL/pgSQL). > > We can talk about other features like temporal, default values, > transactional, > > etc ... > > I feel that if we haven't found a good solution to this in 13 years, we > should assume it is unsolvable and just accept an imperfect solution. > I am sending an reorganized and modified patch set with implementation of session variables Important changes: 1. session variables fence is required everywhere except target of LET command. Then there are no possible collisions of session variables with column or plpgsql variables identifiers. The code related to collision detection or collision solving is removed. 2. I little bit reduced the size of patch because I didn't introduce EXPR_KIND_LET_TARGET and EXPR_KIND_ASSING_TARGET contextes. 3. There are not other changes in the code 4. First two patches (originally 170KB and 160KB) are divided to some few patches with max lengths (81KB and 53KB). The patches are incremental still (and possibly tested incrementally) with more cleaner coverage 0001-introduce-new-class-catalog-pg_variable.patch - new catalog 0002-CREATE-DROP-ALTER-VARIABLE.patch - DDL commands 0003-GRANT-REVOKE-variable.patch - ACL SELECT, UPDATE 0004-support-of-session-variables-for-psql.patch - psql tab complete and \dV 0005-support-of-session-variables-for-pg_dump.patch - pg_dump can backup session variables 0006-session-variable-fences-parsing.patch - parser support for syntax VARIABLE(varname) 0007-local-HASHTAB-for-currently-used-session-variables-a.patch - local memory based storage for values of session variables 0008-collect-session-variables-used-in-plan-and-assign-pa.patch - planner support for reading session variables from query 0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch - executor support for reading session variables from query 0010-svariableReceiver.patch - DestReceiver used by LET command 0011-LET-command-assign-a-result-of-expression-to-the-ses.patch - LET command implementation, parsing, execution 0012-function-pg_session_variables-for-cleaning-tests.patch - debug function - returns a list of session variables from local memory storage 0013-DISCARD-VARIABLES.patch - throw memory used by session variables 0014-memory-cleaning-after-DROP-VARIABLE.patch - clean related memory after DROP VARIABLE 0015-plpgsql-tests.patch - special tests for plpgsql, plan cache, identifier collision, validity of memory used by session variables The new total size of patches is 377KB, the size of previous patches was 400KB. The code is less than 1/3 of this (I think less than 2000 lines). Almost all code is very simple without impact on other parts of Postgres. Maybe an exception is memory-cleaning-after-DROP-VARIABLE patch - but still there is no impact to other parts of Postgres. Now there is a little bit overhead of git format-patch format. I refactorized tests to separate files acl, ddl, dml. U use longer unique identifiers, so the execution of the tests should be more stable and less sensitive for parallel tests execution. I removed a few explicitly redundant tests. This is the first step of implementing session variables in Postgres. It contains only basic implementation of session variables. The lot of proposed features (in this thread) can be implemented in the future without compatibility issues. Their implementation is postponed now, because the patch set is large enough now. Although this is just a subset of proposed (discussed) functionality, I think it can be valuable for people that need session variables. This patchset doesn't contains: 1. Using session variables without variable fences inside plpgsql (then the usage will be the same as PL/pgSQL or PL/SQL variables) 2. Performance optimization of usage of session variables (mostly for plpgsql) 3. Implementation of EXPLAIN LET, PREPARE LET commands 4. Implementation of temporary session variables 5. Possibility to set default value for session variable (now it is NULL) 6. Immutable session variables support 7. Implementation of transactional behave of content of session variables 8. Special option --variable for pg_dump Please, do a review if you can. Comments, notes? Regards Pavel > > -- > Bruce Momjian <[email protected]> https://momjian.us > EDB https://enterprisedb.com > > Do not let urgent matters crowd out time for investment in the future. > Attachments: [text/x-patch] v20250603-0015-plpgsql-tests.patch (11.3K, 3-v20250603-0015-plpgsql-tests.patch) download | inline diff: From 2e4b2a51bcd1b38eed271346d1e51289dc002770 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] v20250603-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (50.5K, 4-v20250603-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 181daa6dbe2c909c8ca6fc21818d000a1169599d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 25 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 238 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1095 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 613608e620d..5d09ad09b80 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5392,10 +5392,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index e14fcd23de9..0ea4a44f1b2 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -512,3 +519,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 53d359c2468..c4474522bc5 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable + * (like base node of assignment indirect) we cannot do permission + * check, because we need read the value (and user can have + * only UPDATE variable). In this case the permission check + * is executed in write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7a229d79dad 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because + * it is not set in subquery. When this variable is used other than + * in base node of assignment indirection, we need to check the access + * rights (and then we need to detect this situation). The variable used + * like base node cannot be different than target (result) variable. + * Because we know the result variable before planner invocation, we + * can simply search of usage just this variable, and we don't need to + * to wait until the end of planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,17 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node + * for assignemnt indirection should be excluded from permission + * check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..09109e80b25 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,28 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by + * the query excluding the variable that is used only as base node + * of assignment indirection. The variable id assigned to this param + * should be same like resultVariable id, and this param should be + * used only once in query. When the variable is referenced by any + * other param, we should to do SELECT permission check for this variable + * too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3731,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3833,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 0d5efea5ca9..3c3785b6d19 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -409,6 +412,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -488,6 +492,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -540,6 +549,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3442,6 +3452,234 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special + * mark, because requires special access when we do + * SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for + * acquiring an AccessShareLock on target variable, setting + * plan dependency and finally for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 28bd26cc922..750cf5171d8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12868,6 +12869,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17970,6 +18003,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18584,6 +18618,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 7fe19a568f1..39d1b6aa610 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4701,6 +4701,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76cde246f63..8c508b851da 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2166,6 +2169,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..dee7e6915a3 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. + * This session variable is used as base node of assignment indirection + * (and it is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 2b1b3ac8a33..423963ab42c 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection + * (when target of LET statement is an array field or an record field). + * For this param we do not check SELECT access right, because this + * param is used just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index ac5b2f22f0f..0951cc1859e 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO svartest_dml_write_only_role; +SET ROLE TO svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO svartest_dml_write_only_role; +SET ROLE TO svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 88cd536162c..59bdffbc602 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO svartest_dml_write_only_role; + +SET ROLE TO svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO svartest_dml_write_only_role; + +SET ROLE TO svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 1e33dfd4589..815689e9401 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] v20250603-0013-DISCARD-VARIABLES.patch (9.6K, 5-v20250603-0013-DISCARD-VARIABLES.patch) download | inline diff: From 0f7493fb3134838c071461712c52c7fa3fd58bfa Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 28 ++++++++++- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 133 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 65100f65e4e..92756d68086 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -102,7 +102,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -691,3 +697,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 750cf5171d8..207a2779df5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2084,7 +2084,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 39d1b6aa610..06cbac40ca0 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4152,7 +4152,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8c508b851da..895d20511b9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 0951cc1859e..6aae1839caa 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -447,3 +447,53 @@ drop cascades to session variable svartest_dml.sesvar46 drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 DROP ROLE svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 59bdffbc602..19cbc373551 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -336,3 +336,27 @@ SELECT VARIABLE(svartest_dml.sesvar48); DROP SCHEMA svartest_dml CASCADE; DROP ROLE svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] v20250603-0012-function-pg_session_variables-for-cleaning-tests.patch (4.6K, 6-v20250603-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 77ba7cc97596b5de60a3bce60f9db0e8281a168a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 93 +++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 +++ 2 files changed, 101 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0ea4a44f1b2..65100f65e4e 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -598,3 +599,95 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] v20250603-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 7-v20250603-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 2577b55c4a50dfcd94fbc8577f4a8cd70295b989 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 92756d68086..4d044d35569 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -75,6 +75,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -91,6 +99,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -122,6 +141,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -175,6 +226,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -204,6 +316,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -327,22 +441,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -403,6 +537,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..0fcc2f67f60 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] v20250603-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.4K, 8-v20250603-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 9303e1276675f0216a8dbad3ec6a6945ec692f7b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execMain.c | 55 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..4f5aef4f3f8 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,36 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..53d359c2468 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,59 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..184de66a48b 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..ac5b2f22f0f --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_read_role; +SET ROLE TO svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO svartest_dml_read_role; +SET ROLE TO svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..88cd536162c --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO svartest_dml_read_role; + +SET ROLE TO svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO svartest_dml_read_role; + +SET ROLE TO svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ce3a8abb623..1e33dfd4589 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2697,6 +2697,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] v20250603-0010-svariableReceiver.patch (8.6K, 9-v20250603-0010-svariableReceiver.patch) download | inline diff: From cd04c98fda800a3e84b48ffcfe3856ffca69a8a4 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + 6 files changed, 204 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c6163fb36f6 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- -- 2.49.0 [text/x-patch] v20250603-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250603-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 3acf821788372a89ec73f6307354b4024219f954 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebe..714f58bd3d7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] v20250603-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250603-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From b1282573a2579c4bfce11397b32a38c774ce3581 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 426 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 434 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..e14fcd23de9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,440 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdcdbd3f57f..ce3a8abb623 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2639,6 +2639,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] v20250603-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 12-v20250603-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 3e2342f9d4191cdb1e6377dda1385c8c24e9238d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 175fe9c4273..9bf77642702 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd..5d8762538bd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11558,6 +11741,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16012,6 +16198,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19750,6 +19939,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec499bcf287..bdcdbd3f57f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3187,6 +3187,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] v20250603-0006-session-variable-fences-parsing.patch (32.2K, 13-v20250603-0006-session-variable-fences-parsing.patch) download | inline diff: From 26067f799b83ed578b077d8bf113895783c5bb4c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 9 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- 17 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 66fa267facc..613608e620d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5385,6 +5385,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4307cad15c7..beafee2a341 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..c516e8f36b8 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,15 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. This case should be blocked parser + * by expr_kind_allows_session_variables, so only assertions is + * used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a16fdd65601..0d5efea5ca9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -687,6 +687,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1112,6 +1113,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1577,6 +1579,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1803,6 +1806,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2054,6 +2058,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2529,6 +2534,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2596,6 +2602,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -3072,6 +3079,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 61a5f762d85..28bd26cc922 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -882,7 +882,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15659,6 +15659,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17045,6 +17058,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 1f8e2d54673..819ddb0557b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3118,6 +3255,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..381c8e74fb8 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 191713de7f4..76cde246f63 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f26..2b1b3ac8a33 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +400,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fb..cb4eb8418b7 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -244,6 +244,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* -- 2.49.0 [text/x-patch] v20250603-0003-GRANT-REVOKE-variable.patch (52.7K, 14-v20250603-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 173dbb2ee453c010a52c0b5e68dd2c5784c069a4 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 6ada59c798a..23da444f18f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3366,7 +3366,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index dfbf7dbab4d..66fa267facc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2015,6 +2015,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2050,6 +2051,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2293,7 +2296,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2308,7 +2312,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2495,6 +2500,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5364,6 +5375,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c67688cbf5f..3e8d4bea149 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25602,6 +25602,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25841,8 +25860,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26110,6 +26129,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 64db4b5e88b..61a5f762d85 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -785,7 +785,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7966,6 +7966,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8011,6 +8019,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8209,6 +8225,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18101,6 +18118,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18758,6 +18776,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] v20250603-0004-support-of-session-variables-for-psql.patch (22.7K, 15-v20250603-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 2e91a9d3740a0614111819f3c61d575ebdddec90 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3e8d4bea149..fa09bf48fb7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26130,6 +26130,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca0..f86966f5128 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2135,6 +2135,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index f608c23c3df..4307cad15c7 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..a171bf5f3b4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1246,6 +1246,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e..ad46ca4f9be 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1223,7 +1223,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1240,6 +1240,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5313,6 +5315,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a7..81886eb9725 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -261,6 +261,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec65ab79fec..7fe19a568f1 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3985,6 +3996,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4262,6 +4280,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4463,7 +4487,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4471,6 +4497,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4485,7 +4512,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4493,7 +4521,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4529,6 +4558,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4831,7 +4862,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5320,6 +5351,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2..fd6b2a6b8c8 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f0..b567ee1eee2 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] v20250603-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 16-v20250603-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 7c5e2b1a7bf0d174a4d805ca5ee03bfdf8f38b15 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index cbd4e40a320..6ada59c798a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9779,4 +9784,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633..26fc9f6810d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -907,6 +907,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -966,6 +967,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 [text/x-patch] v20250603-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 17-v20250603-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From ecad46cd2f10de68bf5a6d505042302dfebfd004 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fcd1cb85352..dfbf7dbab4d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5351,6 +5351,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..f608c23c3df 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index acf11e83c04..0e1e3923433 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..f8fefdc2cf8 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables + * from other sessions, so better to fail if there are any + * session variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0b5652071d1..64db4b5e88b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -285,8 +286,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -783,8 +784,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1050,6 +1051,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1592,6 +1594,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5267,6 +5270,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7058,6 +7089,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9965,6 +9997,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10326,6 +10376,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10607,6 +10675,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18024,6 +18100,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18680,6 +18757,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index dd00ab420b8..191713de7f4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 26fc9f6810d..ec499bcf287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -562,6 +562,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-03 11:43 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-03 11:43 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi fix badly named role - needs prefix regress_ CREATE ROLE svartest_dml_read_role; +WARNING: roles created by regression test cases should have names starting with "regress_" CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() RETURNS int AS $$ Attachments: [text/x-patch] v20250603-2-0013-DISCARD-VARIABLES.patch (9.6K, 3-v20250603-2-0013-DISCARD-VARIABLES.patch) download | inline diff: From 8470dc3acfe571ae224c1d8c99f499217f626126 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 28 ++++++++++- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 133 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 65100f65e4e..92756d68086 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -102,7 +102,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -691,3 +697,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 750cf5171d8..207a2779df5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2084,7 +2084,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 39d1b6aa610..06cbac40ca0 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4152,7 +4152,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8c508b851da..895d20511b9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 0fdf0fdc68a..447c10bc4a3 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -447,3 +447,53 @@ drop cascades to session variable svartest_dml.sesvar46 drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 1250e7ef062..340ae6f76ab 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -336,3 +336,27 @@ SELECT VARIABLE(svartest_dml.sesvar48); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] v20250603-2-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 4-v20250603-2-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 661a43527172ae7df3f46a2c36cf8deaa8e2f3b2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 92756d68086..4d044d35569 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -75,6 +75,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -91,6 +99,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -122,6 +141,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -175,6 +226,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -204,6 +316,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -327,22 +441,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -403,6 +537,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..0fcc2f67f60 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] v20250603-2-0012-function-pg_session_variables-for-cleaning-tests.patch (4.6K, 5-v20250603-2-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From e00947a6752d9b5e097a4bb662ced88213008356 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 93 +++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 +++ 2 files changed, 101 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0ea4a44f1b2..65100f65e4e 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -598,3 +599,95 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] v20250603-2-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (50.6K, 6-v20250603-2-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 27ebf72dc5c9b1bfc0d51608523405189cd9a3af Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 25 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 238 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1095 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 613608e620d..5d09ad09b80 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5392,10 +5392,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index e14fcd23de9..0ea4a44f1b2 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -512,3 +519,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 53d359c2468..c4474522bc5 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable + * (like base node of assignment indirect) we cannot do permission + * check, because we need read the value (and user can have + * only UPDATE variable). In this case the permission check + * is executed in write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7a229d79dad 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because + * it is not set in subquery. When this variable is used other than + * in base node of assignment indirection, we need to check the access + * rights (and then we need to detect this situation). The variable used + * like base node cannot be different than target (result) variable. + * Because we know the result variable before planner invocation, we + * can simply search of usage just this variable, and we don't need to + * to wait until the end of planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,17 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node + * for assignemnt indirection should be excluded from permission + * check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..09109e80b25 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,28 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by + * the query excluding the variable that is used only as base node + * of assignment indirection. The variable id assigned to this param + * should be same like resultVariable id, and this param should be + * used only once in query. When the variable is referenced by any + * other param, we should to do SELECT permission check for this variable + * too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3731,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3833,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 0d5efea5ca9..3c3785b6d19 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -409,6 +412,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -488,6 +492,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -540,6 +549,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3442,6 +3452,234 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special + * mark, because requires special access when we do + * SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for + * acquiring an AccessShareLock on target variable, setting + * plan dependency and finally for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 28bd26cc922..750cf5171d8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12868,6 +12869,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17970,6 +18003,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18584,6 +18618,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 7fe19a568f1..39d1b6aa610 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4701,6 +4701,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76cde246f63..8c508b851da 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2166,6 +2169,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..dee7e6915a3 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. + * This session variable is used as base node of assignment indirection + * (and it is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 2b1b3ac8a33..423963ab42c 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection + * (when target of LET statement is an array field or an record field). + * For this param we do not check SELECT access right, because this + * param is used just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..0fdf0fdc68a 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..1250e7ef062 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 1e33dfd4589..815689e9401 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] v20250603-2-0015-plpgsql-tests.patch (11.3K, 7-v20250603-2-0015-plpgsql-tests.patch) download | inline diff: From 6242d3640477e4e5800bac61c7112e45c60eab78 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] v20250603-2-0010-svariableReceiver.patch (8.6K, 8-v20250603-2-0010-svariableReceiver.patch) download | inline diff: From ec8f923e88e576b50fb40bd3d304f2050933e548 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + 6 files changed, 204 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c6163fb36f6 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- -- 2.49.0 [text/x-patch] v20250603-2-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 9-v20250603-2-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 3acf821788372a89ec73f6307354b4024219f954 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebe..714f58bd3d7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] v20250603-2-0006-session-variable-fences-parsing.patch (32.2K, 10-v20250603-2-0006-session-variable-fences-parsing.patch) download | inline diff: From 26067f799b83ed578b077d8bf113895783c5bb4c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 9 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- 17 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 66fa267facc..613608e620d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5385,6 +5385,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4307cad15c7..beafee2a341 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..c516e8f36b8 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,15 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. This case should be blocked parser + * by expr_kind_allows_session_variables, so only assertions is + * used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a16fdd65601..0d5efea5ca9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -687,6 +687,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1112,6 +1113,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1577,6 +1579,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1803,6 +1806,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2054,6 +2058,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2529,6 +2534,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2596,6 +2602,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -3072,6 +3079,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 61a5f762d85..28bd26cc922 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -882,7 +882,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15659,6 +15659,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17045,6 +17058,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 1f8e2d54673..819ddb0557b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3118,6 +3255,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..381c8e74fb8 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 191713de7f4..76cde246f63 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f26..2b1b3ac8a33 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +400,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fb..cb4eb8418b7 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -244,6 +244,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* -- 2.49.0 [text/x-patch] v20250603-2-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 11-v20250603-2-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 1b49311d8a26c3492935674e78dd6d76d2761b4a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execMain.c | 55 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..4f5aef4f3f8 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,36 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..53d359c2468 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,59 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..184de66a48b 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ce3a8abb623..1e33dfd4589 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2697,6 +2697,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] v20250603-2-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 12-v20250603-2-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From b1282573a2579c4bfce11397b32a38c774ce3581 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 426 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 434 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..e14fcd23de9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,440 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdcdbd3f57f..ce3a8abb623 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2639,6 +2639,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] v20250603-2-0004-support-of-session-variables-for-psql.patch (22.7K, 13-v20250603-2-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 2e91a9d3740a0614111819f3c61d575ebdddec90 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3e8d4bea149..fa09bf48fb7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26130,6 +26130,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca0..f86966f5128 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2135,6 +2135,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index f608c23c3df..4307cad15c7 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..a171bf5f3b4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1246,6 +1246,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e..ad46ca4f9be 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1223,7 +1223,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1240,6 +1240,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5313,6 +5315,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a7..81886eb9725 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -261,6 +261,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec65ab79fec..7fe19a568f1 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3985,6 +3996,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4262,6 +4280,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4463,7 +4487,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4471,6 +4497,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4485,7 +4512,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4493,7 +4521,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4529,6 +4558,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4831,7 +4862,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5320,6 +5351,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2..fd6b2a6b8c8 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f0..b567ee1eee2 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] v20250603-2-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 14-v20250603-2-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 3e2342f9d4191cdb1e6377dda1385c8c24e9238d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 175fe9c4273..9bf77642702 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd..5d8762538bd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11558,6 +11741,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16012,6 +16198,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19750,6 +19939,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec499bcf287..bdcdbd3f57f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3187,6 +3187,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] v20250603-2-0003-GRANT-REVOKE-variable.patch (52.7K, 15-v20250603-2-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 173dbb2ee453c010a52c0b5e68dd2c5784c069a4 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 6ada59c798a..23da444f18f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3366,7 +3366,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index dfbf7dbab4d..66fa267facc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2015,6 +2015,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2050,6 +2051,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2293,7 +2296,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2308,7 +2312,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2495,6 +2500,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5364,6 +5375,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c67688cbf5f..3e8d4bea149 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25602,6 +25602,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25841,8 +25860,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26110,6 +26129,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 64db4b5e88b..61a5f762d85 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -785,7 +785,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7966,6 +7966,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8011,6 +8019,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8209,6 +8225,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18101,6 +18118,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18758,6 +18776,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] v20250603-2-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250603-2-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From ecad46cd2f10de68bf5a6d505042302dfebfd004 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fcd1cb85352..dfbf7dbab4d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5351,6 +5351,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..f608c23c3df 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index acf11e83c04..0e1e3923433 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..f8fefdc2cf8 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables + * from other sessions, so better to fail if there are any + * session variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0b5652071d1..64db4b5e88b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -285,8 +286,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -783,8 +784,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1050,6 +1051,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1592,6 +1594,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5267,6 +5270,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7058,6 +7089,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9965,6 +9997,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10326,6 +10376,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10607,6 +10675,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18024,6 +18100,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18680,6 +18757,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index dd00ab420b8..191713de7f4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 26fc9f6810d..ec499bcf287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -562,6 +562,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 [text/x-patch] v20250603-2-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250603-2-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 7c5e2b1a7bf0d174a4d805ca5ee03bfdf8f38b15 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index cbd4e40a320..6ada59c798a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9779,4 +9784,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633..26fc9f6810d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -907,6 +907,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -966,6 +967,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-04 20:22 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-04 20:22 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi I found a small problem in the pg_session_variables function. I expected syscache to be flushed for recent catalog changes before any usage of this function. But it is not always true. When I used the DCATCACHE_FORCE_RELEASE build option, I got different results from isolation tests. Fix is simple - just call AcceptInvalidationMessages(); before touching syscache in this function. Note: pg_session_variables is a debug function - it shows entries of a hash table with session variables used in the current session. The result's attribute with information if the entry is related to a valid or to invalid value (related tuple in pg_variable exists or not) was not correct. Regards Pavel Attachments: [text/x-patch] v20250604-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 3-v20250604-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 7f3bba5e00853adcdec5f42ccbf6f7c6d056974f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 4339dc043e8..3c142041db5 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -75,6 +75,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -91,6 +99,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -122,6 +141,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -175,6 +226,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again + * by concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -204,6 +316,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -327,22 +441,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still be - * valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is already + * locked, and remove_invalid_session_variables() would remove + * variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -403,6 +537,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] v20250604-0015-plpgsql-tests.patch (11.3K, 4-v20250604-0015-plpgsql-tests.patch) download | inline diff: From 5689ae72c4951266b226b15969f2e939e2801802 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] v20250604-0013-DISCARD-VARIABLES.patch (9.6K, 5-v20250604-0013-DISCARD-VARIABLES.patch) download | inline diff: From d4366c77ecf252fe75d74490a2a334de9faf8f2e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 28 ++++++++++- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 133 insertions(+), 3 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 41f470caa86..4339dc043e8 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -102,7 +102,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -698,3 +704,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 750cf5171d8..207a2779df5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2084,7 +2084,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 39d1b6aa610..06cbac40ca0 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4152,7 +4152,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8c508b851da..895d20511b9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 0fdf0fdc68a..447c10bc4a3 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -447,3 +447,53 @@ drop cascades to session variable svartest_dml.sesvar46 drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 1250e7ef062..340ae6f76ab 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -336,3 +336,27 @@ SELECT VARIABLE(svartest_dml.sesvar48); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] v20250604-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (50.6K, 6-v20250604-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From c01ba143459210ed48842191d175a2e03b54df73 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 25 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 238 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1095 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 613608e620d..5d09ad09b80 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5392,10 +5392,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index e14fcd23de9..0ea4a44f1b2 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -512,3 +519,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 53d359c2468..c4474522bc5 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable + * (like base node of assignment indirect) we cannot do permission + * check, because we need read the value (and user can have + * only UPDATE variable). In this case the permission check + * is executed in write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7a229d79dad 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because + * it is not set in subquery. When this variable is used other than + * in base node of assignment indirection, we need to check the access + * rights (and then we need to detect this situation). The variable used + * like base node cannot be different than target (result) variable. + * Because we know the result variable before planner invocation, we + * can simply search of usage just this variable, and we don't need to + * to wait until the end of planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,17 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node + * for assignemnt indirection should be excluded from permission + * check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..09109e80b25 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,28 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by + * the query excluding the variable that is used only as base node + * of assignment indirection. The variable id assigned to this param + * should be same like resultVariable id, and this param should be + * used only once in query. When the variable is referenced by any + * other param, we should to do SELECT permission check for this variable + * too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3731,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3833,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 0d5efea5ca9..3c3785b6d19 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -409,6 +412,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -488,6 +492,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -540,6 +549,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3442,6 +3452,234 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special + * mark, because requires special access when we do + * SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for + * acquiring an AccessShareLock on target variable, setting + * plan dependency and finally for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 28bd26cc922..750cf5171d8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12868,6 +12869,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17970,6 +18003,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18584,6 +18618,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 7fe19a568f1..39d1b6aa610 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4701,6 +4701,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76cde246f63..8c508b851da 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2166,6 +2169,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..dee7e6915a3 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. + * This session variable is used as base node of assignment indirection + * (and it is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 2b1b3ac8a33..423963ab42c 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection + * (when target of LET statement is an array field or an record field). + * For this param we do not check SELECT access right, because this + * param is used just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..0fdf0fdc68a 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..1250e7ef062 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 1e33dfd4589..815689e9401 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] v20250604-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 7-v20250604-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 2405007608306d3498361096cc884786396962c1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 0ea4a44f1b2..41f470caa86 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -598,3 +599,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. + * For stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] v20250604-0010-svariableReceiver.patch (8.6K, 8-v20250604-0010-svariableReceiver.patch) download | inline diff: From 0f5203db5ddd805c46d95bf74804452a513b6505 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + 6 files changed, 204 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c6163fb36f6 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- -- 2.49.0 [text/x-patch] v20250604-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250604-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From c8c95a9232471ea7b5784dd98690630c3f4e56c1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 30 +++ src/backend/executor/execMain.c | 55 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..4f5aef4f3f8 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,36 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..53d359c2468 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,59 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..184de66a48b 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ce3a8abb623..1e33dfd4589 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2697,6 +2697,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] v20250604-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250604-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 1be0b03973210d631916ca5f1c2bfb92a36bfbf8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebe..714f58bd3d7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] v20250604-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250604-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 5494a4ac6ba6e5da485037d515e3777439679f73 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 426 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 434 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..e14fcd23de9 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,440 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdcdbd3f57f..ce3a8abb623 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2639,6 +2639,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] v20250604-0006-session-variable-fences-parsing.patch (32.2K, 12-v20250604-0006-session-variable-fences-parsing.patch) download | inline diff: From 09263e60da9cc0f1936c1ca5ae9d3e3c4ec3d790 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 9 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- 17 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 66fa267facc..613608e620d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5385,6 +5385,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4307cad15c7..beafee2a341 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..c516e8f36b8 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,15 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. This case should be blocked parser + * by expr_kind_allows_session_variables, so only assertions is + * used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a16fdd65601..0d5efea5ca9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -687,6 +687,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1112,6 +1113,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1577,6 +1579,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1803,6 +1806,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2054,6 +2058,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2529,6 +2534,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2596,6 +2602,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -3072,6 +3079,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 61a5f762d85..28bd26cc922 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -882,7 +882,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15659,6 +15659,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17045,6 +17058,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 1f8e2d54673..819ddb0557b 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3118,6 +3255,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..381c8e74fb8 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 191713de7f4..76cde246f63 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f26..2b1b3ac8a33 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +400,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fb..cb4eb8418b7 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -244,6 +244,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* -- 2.49.0 [text/x-patch] v20250604-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250604-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 255a418bd3d2ac89e7cd6ab84a575194c495c0c6 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 197c1295d93..443869725d2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd..5d8762538bd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11558,6 +11741,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16012,6 +16198,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19750,6 +19939,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec499bcf287..bdcdbd3f57f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3187,6 +3187,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] v20250604-0004-support-of-session-variables-for-psql.patch (22.7K, 14-v20250604-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 6e2e7bbf562da3bceb9cd94ead171a92d81759ce Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3e8d4bea149..fa09bf48fb7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26130,6 +26130,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca0..f86966f5128 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2135,6 +2135,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index f608c23c3df..4307cad15c7 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..a171bf5f3b4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1246,6 +1246,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e..ad46ca4f9be 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1223,7 +1223,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1240,6 +1240,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5313,6 +5315,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a7..81886eb9725 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -261,6 +261,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec65ab79fec..7fe19a568f1 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3985,6 +3996,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4262,6 +4280,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4463,7 +4487,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4471,6 +4497,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4485,7 +4512,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4493,7 +4521,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4529,6 +4558,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4831,7 +4862,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5320,6 +5351,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2..fd6b2a6b8c8 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f0..b567ee1eee2 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] v20250604-0003-GRANT-REVOKE-variable.patch (52.7K, 15-v20250604-0003-GRANT-REVOKE-variable.patch) download | inline diff: From ae6f977d3e8885a6551e337af565ab136f50cb8d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 6ada59c798a..23da444f18f 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3366,7 +3366,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index dfbf7dbab4d..66fa267facc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2015,6 +2015,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2050,6 +2051,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2293,7 +2296,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2308,7 +2312,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2495,6 +2500,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5364,6 +5375,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c67688cbf5f..3e8d4bea149 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25602,6 +25602,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25841,8 +25860,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26110,6 +26129,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 64db4b5e88b..61a5f762d85 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -785,7 +785,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7966,6 +7966,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8011,6 +8019,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8209,6 +8225,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18101,6 +18118,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18758,6 +18776,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] v20250604-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 16-v20250604-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 670f39b285c5ef2f543e1268f18b123a8861b478 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index cbd4e40a320..6ada59c798a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9779,4 +9784,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633..26fc9f6810d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -907,6 +907,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -966,6 +967,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 [text/x-patch] v20250604-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 17-v20250604-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From d7f5d826d95d1fce798257b533cf209c2b700821 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fcd1cb85352..dfbf7dbab4d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5351,6 +5351,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..f608c23c3df 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We + * don't expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index acf11e83c04..0e1e3923433 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..f8fefdc2cf8 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables + * from other sessions, so better to fail if there are any + * session variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0b5652071d1..64db4b5e88b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -285,8 +286,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -783,8 +784,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1050,6 +1051,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1592,6 +1594,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5267,6 +5270,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7058,6 +7089,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9965,6 +9997,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10326,6 +10376,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10607,6 +10675,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18024,6 +18100,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18680,6 +18757,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index dd00ab420b8..191713de7f4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 26fc9f6810d..ec499bcf287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -562,6 +562,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-10 14:25 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-10 14:25 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi I run pgindent - the changes are mostly only in comments Regards Pavel Attachments: [text/x-patch] 0015-plpgsql-tests.patch (11.3K, 3-0015-plpgsql-tests.patch) download | inline diff: From 9cf18e094a46cee0cc6451b53d838805d50e1e2c Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] 0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 4-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 6f069da3ce73ded3f09cd2ac9c77d391bb9244c3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] 0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (50.6K, 5-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From e98c52816028de792ee3953eb83d9ad6fa54651e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1093 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 613608e620d..5d09ad09b80 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5392,10 +5392,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7119ceecafb 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 0d5efea5ca9..2a5ea22917f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -409,6 +412,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -488,6 +492,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -540,6 +549,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3442,6 +3452,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 28bd26cc922..750cf5171d8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12868,6 +12869,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17970,6 +18003,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18584,6 +18618,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 7fe19a568f1..39d1b6aa610 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4701,6 +4701,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76cde246f63..8c508b851da 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2166,6 +2169,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 2b1b3ac8a33..fa33f4f1bab 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..0fdf0fdc68a 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..1250e7ef062 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 0c859cfe187..b08ead23a79 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] 0013-DISCARD-VARIABLES.patch (10.1K, 6-0013-DISCARD-VARIABLES.patch) download | inline diff: From f7e5a21f7117ee0d179b074a738ef522fd455b5a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 750cf5171d8..207a2779df5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2084,7 +2084,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 39d1b6aa610..06cbac40ca0 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4152,7 +4152,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8c508b851da..895d20511b9 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 0fdf0fdc68a..447c10bc4a3 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -447,3 +447,53 @@ drop cascades to session variable svartest_dml.sesvar46 drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 1250e7ef062..340ae6f76ab 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -336,3 +336,27 @@ SELECT VARIABLE(svartest_dml.sesvar48); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] 0010-svariableReceiver.patch (8.9K, 7-0010-svariableReceiver.patch) download | inline diff: From 369c4d58786cc9c12655ab2a2e0acb7882b5fdab Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 5cda2e0d2ec..0c859cfe187 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2641,6 +2641,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] 0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 8-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From d9e0a463a9175a0d6d7c06620fdd308f092f6f96 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] 0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 9-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 37359188ac0c51c58da2938301fe448f22135ef3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebe..714f58bd3d7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] 0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 10-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 017f744c38f1136bc9f69376846d70b30dc4676f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..4fb7b46ccff 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index cdca4963f3e..5cda2e0d2ec 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2697,6 +2697,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] 0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From fd4cb0b783a1838d0830917cd2c5a63a320452a9 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 661b27e5f2c..cdca4963f3e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2639,6 +2639,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] 0005-support-of-session-variables-for-pg_dump.patch (15.3K, 12-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 7710f9bacd1b5a725403f11b6812a7843d4c59e1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 197c1295d93..443869725d2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd..5d8762538bd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11558,6 +11741,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16012,6 +16198,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19750,6 +19939,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec499bcf287..bdcdbd3f57f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3187,6 +3187,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] 0006-session-variable-fences-parsing.patch (32.5K, 13-0006-session-variable-fences-parsing.patch) download | inline diff: From 89b9de356c9969741b5bb821daa5133da716d854 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 66fa267facc..613608e620d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5385,6 +5385,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a16fdd65601..0d5efea5ca9 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -687,6 +687,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1112,6 +1113,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1577,6 +1579,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1803,6 +1806,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2054,6 +2058,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2529,6 +2534,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2596,6 +2602,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -3072,6 +3079,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 61a5f762d85..28bd26cc922 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -882,7 +882,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15659,6 +15659,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17045,6 +17058,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 1f8e2d54673..0b739a71a2f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3118,6 +3255,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 191713de7f4..76cde246f63 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f26..2b1b3ac8a33 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +400,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fb..cb4eb8418b7 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -244,6 +244,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdcdbd3f57f..661b27e5f2c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3179,6 +3179,7 @@ ValidatorModuleResult ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.49.0 [text/x-patch] 0003-GRANT-REVOKE-variable.patch (52.7K, 14-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 62a2fb4b8e52091224daf530dc900d2dae930cd0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index dfe8eeabdf8..55957d742bc 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index dfbf7dbab4d..66fa267facc 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2015,6 +2015,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2050,6 +2051,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2293,7 +2296,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2308,7 +2312,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2495,6 +2500,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5364,6 +5375,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c67688cbf5f..3e8d4bea149 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25602,6 +25602,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25841,8 +25860,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26110,6 +26129,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 64db4b5e88b..61a5f762d85 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -785,7 +785,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7966,6 +7966,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8011,6 +8019,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8209,6 +8225,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18101,6 +18118,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18758,6 +18776,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] 0004-support-of-session-variables-for-psql.patch (22.7K, 15-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 6833e4a4d418ad83bc5db67f8326f6198eba67df Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3e8d4bea149..fa09bf48fb7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26130,6 +26130,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca0..f86966f5128 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2135,6 +2135,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..a171bf5f3b4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1246,6 +1246,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 24e0100c9f0..97bade6dc56 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1223,7 +1223,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1240,6 +1240,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5313,6 +5315,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a7..81886eb9725 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -261,6 +261,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec65ab79fec..7fe19a568f1 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3985,6 +3996,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4262,6 +4280,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4463,7 +4487,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4471,6 +4497,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4485,7 +4512,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4493,7 +4521,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4529,6 +4558,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4831,7 +4862,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5320,6 +5351,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2..fd6b2a6b8c8 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f0..b567ee1eee2 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] 0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 8ea9617954af4cc9ae9a1f160b64319c7faca4a2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fcd1cb85352..dfbf7dbab4d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5351,6 +5351,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index ea96947d813..230b6966d2d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..8f8b7d09f32 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0b5652071d1..64db4b5e88b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -285,8 +286,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -783,8 +784,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1050,6 +1051,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1592,6 +1594,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5267,6 +5270,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7058,6 +7089,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9965,6 +9997,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10326,6 +10376,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10607,6 +10675,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18024,6 +18100,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18680,6 +18757,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index dd00ab420b8..191713de7f4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 26fc9f6810d..ec499bcf287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -562,6 +562,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 [text/x-patch] 0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From a3f03411243089f381a56824025c474c00f68ade Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index fa86c569dc4..dfe8eeabdf8 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9773,4 +9778,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633..26fc9f6810d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -907,6 +907,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -966,6 +967,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-13 04:53 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-13 04:53 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi rebase after 0f65f3eec478db8ac4f217a608b4478fed023bac Reards Pavel Attachments: [text/x-patch] v20250613-0015-plpgsql-tests.patch (11.3K, 3-v20250613-0015-plpgsql-tests.patch) download | inline diff: From 196909af97057d1db6882de3e11ee406a823c093 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] v20250613-0013-DISCARD-VARIABLES.patch (10.1K, 4-v20250613-0013-DISCARD-VARIABLES.patch) download | inline diff: From 3b569cf4dc3146483f9d9db7a6cce619b091e728 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f4ac5186348..4b9a521e697 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2083,7 +2083,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index c689a45918c..d2f6314d1b4 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4153,7 +4153,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 1dd7f3ea00f..7602cefcfc2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 0fdf0fdc68a..447c10bc4a3 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -447,3 +447,53 @@ drop cascades to session variable svartest_dml.sesvar46 drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 1250e7ef062..340ae6f76ab 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -336,3 +336,27 @@ SELECT VARIABLE(svartest_dml.sesvar48); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] v20250613-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 5-v20250613-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 4acf7b188fb52ad4701f06ae57b1a00c6b96dc35 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] v20250613-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 6-v20250613-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 8926b20ca475fcdd269516e211091d09faeec9ff Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] v20250613-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (50.5K, 7-v20250613-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 4ecb5e987a970195c4787ea9224d2a5b577ed2da Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 +++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 ++++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 258 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 177 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1093 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index bcb62729771..df4eac5af8d 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5393,10 +5393,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 714f58bd3d7..7119ceecafb 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6d7c8c56c90..f4ac5186348 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12862,6 +12863,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17937,6 +17970,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18551,6 +18585,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 78fc8ef6d45..c689a45918c 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4702,6 +4702,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4654574c06c..1dd7f3ea00f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index dc592602c65..0d1233f59e9 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -402,6 +402,15 @@ typedef struct Param Oid paramcollid pg_node_attr(query_jumble_ignore); /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..0fdf0fdc68a 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,261 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 13 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..1250e7ef062 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,180 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 0c859cfe187..b08ead23a79 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1542,6 +1542,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] v20250613-0010-svariableReceiver.patch (8.9K, 8-v20250613-0010-svariableReceiver.patch) download | inline diff: From 43c4912e138c3d95e22149f7175826d18e79ceb7 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 5cda2e0d2ec..0c859cfe187 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2641,6 +2641,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] v20250613-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 9-v20250613-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 56b3f33d7d9f4a5c37b8106fbc065eb862040d25 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 661b27e5f2c..cdca4963f3e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2639,6 +2639,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SampleScan SampleScanGetSampleSize_function -- 2.49.0 [text/x-patch] v20250613-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 10-v20250613-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 47fca196ca0f89ae4cc600005a85229fef57d1e0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..4fb7b46ccff 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index cdca4963f3e..5cda2e0d2ec 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2697,6 +2697,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] v20250613-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 11-v20250613-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 9216f2f5db74333bcab6a37904b584a09be6414f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebe..714f58bd3d7 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] v20250613-0006-session-variable-fences-parsing.patch (32.5K, 12-v20250613-0006-session-variable-fences-parsing.patch) download | inline diff: From 6d4af4b9df30a5cba7bd1b0970078470418b099f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 4d79ac96e62..bcb62729771 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5386,6 +5386,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 01a9731ce28..6d7c8c56c90 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -524,7 +524,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15637,6 +15637,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17012,6 +17025,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8abe3f0d409..4654574c06c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 01510b01b64..dc592602c65 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +400,8 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdcdbd3f57f..661b27e5f2c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3179,6 +3179,7 @@ ValidatorModuleResult ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.49.0 [text/x-patch] v20250613-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250613-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 7b032bd93e60b4b274f7e147c7ee792c18396e9b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 197c1295d93..443869725d2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd..5d8762538bd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11558,6 +11741,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16012,6 +16198,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19750,6 +19939,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec499bcf287..bdcdbd3f57f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3187,6 +3187,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] v20250613-0003-GRANT-REVOKE-variable.patch (52.7K, 14-v20250613-0003-GRANT-REVOKE-variable.patch) download | inline diff: From cbd3e2d52dfca2bec325d35f99c16127654b5132 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index dfe8eeabdf8..55957d742bc 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index aeb20c3b6c0..4d79ac96e62 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2015,6 +2015,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2050,6 +2051,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2294,7 +2297,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2309,7 +2313,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2496,6 +2501,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5365,6 +5376,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c67688cbf5f..3e8d4bea149 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25602,6 +25602,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25841,8 +25860,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26110,6 +26129,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 92bf8419833..01a9731ce28 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7964,6 +7964,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8009,6 +8017,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8207,6 +8223,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18068,6 +18085,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18725,6 +18743,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] v20250613-0004-support-of-session-variables-for-psql.patch (22.7K, 15-v20250613-0004-support-of-session-variables-for-psql.patch) download | inline diff: From c290d80d697872b8168b3fb186fb1f98ca3f4495 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 3e8d4bea149..fa09bf48fb7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26130,6 +26130,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1cb67ff806d..3813d094436 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2135,6 +2135,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba..a171bf5f3b4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1246,6 +1246,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 24e0100c9f0..97bade6dc56 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1223,7 +1223,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1240,6 +1240,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5313,6 +5315,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a7..81886eb9725 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -261,6 +261,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 2c0b4f28c14..78fc8ef6d45 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3986,6 +3997,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4263,6 +4281,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4464,7 +4488,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4472,6 +4498,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4486,7 +4513,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4494,7 +4522,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4530,6 +4559,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4832,7 +4863,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5321,6 +5352,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2..fd6b2a6b8c8 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f0..b567ee1eee2 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] v20250613-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 16-v20250613-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From a358e10efc1c5385c1e7023533f911d9eebbc8eb Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index fa86c569dc4..dfe8eeabdf8 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9773,4 +9778,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633..26fc9f6810d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -907,6 +907,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -966,6 +967,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 [text/x-patch] v20250613-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 17-v20250613-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From f24e7d21125373df02ece921cb85de3724c7f02f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 96936bcd3ae..aeb20c3b6c0 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5352,6 +5352,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index ea96947d813..230b6966d2d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..8f8b7d09f32 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 50f53159d58..92bf8419833 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1591,6 +1593,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5265,6 +5268,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7056,6 +7087,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9963,6 +9995,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10324,6 +10374,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10605,6 +10673,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -17991,6 +18067,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18647,6 +18724,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ba12678d1cb..8abe3f0d409 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 26fc9f6810d..ec499bcf287 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -562,6 +562,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-06-25 17:33 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-06-25 17:33 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi * fresh rebase + new small test of usage LET from extended query protocol (test of usage of parameters) + few notes in patch 11 commit message about reasons why LET keyword was used instead SET for assign command Regards Pavel Attachments: [text/x-patch] v20250625-0015-plpgsql-tests.patch (11.3K, 3-v20250625-0015-plpgsql-tests.patch) download | inline diff: From 0ea1e432513af9586ba8a8f9226689a5c4829bb1 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.49.0 [text/x-patch] v20250625-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 4-v20250625-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From f96a7d759a4dabc25edc39839b73fae30e6c8c4d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f72acdd84bd..d6cee572561 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12579,4 +12579,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.49.0 [text/x-patch] v20250625-0013-DISCARD-VARIABLES.patch (10.1K, 5-v20250625-0013-DISCARD-VARIABLES.patch) download | inline diff: From 7adc02a4ae2036625accb27404ca0fc35b4d42b9 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index f4ac5186348..4b9a521e697 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2083,7 +2083,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 20e4d43576b..5ffcee5aa83 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2961,6 +2961,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 61b643699f9..019687fcb72 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4153,7 +4153,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 1dd7f3ea00f..7602cefcfc2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4071,6 +4071,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.49.0 [text/x-patch] v20250625-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 6-v20250625-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 8ceaca6f34f7daa38c986a763b1def0fc7be52c1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 5b95bb0d8e5..5a47b5c04fe 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6d7c8c56c90..f4ac5186348 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12862,6 +12863,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17937,6 +17970,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18551,6 +18585,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 782b022da9c..20e4d43576b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1068,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2212,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2414,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3303,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 1579748ed75..61b643699f9 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4702,6 +4702,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4654574c06c..1dd7f3ea00f 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ec3f086b557..de1deff5ced 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.49.0 [text/x-patch] v20250625-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 7-v20250625-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From cc0eb39e75ad591cc516ed52749311797c25a855 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.49.0 [text/x-patch] v20250625-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 8-v20250625-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From c78761db5afd0f626aaa75b989f10615cf406c50 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213f..4fb7b46ccff 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -641,6 +641,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -700,6 +710,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a796cedf28c..53712f8a652 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2702,6 +2702,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.49.0 [text/x-patch] v20250625-0010-svariableReceiver.patch (8.9K, 9-v20250625-0010-svariableReceiver.patch) download | inline diff: From 53e22e96ee7c58c9acaca74bc8c4407db1fdd238 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 53712f8a652..ec3f086b557 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2645,6 +2645,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.49.0 [text/x-patch] v20250625-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 10-v20250625-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 774ade3749b9fb07af6039078f4b0b6960a62e89 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9646f6b5230..a796cedf28c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2643,6 +2643,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.49.0 [text/x-patch] v20250625-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 11-v20250625-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From c73812de866786459732f7591c89c319e98556d0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 549aedcfa99..5b95bb0d8e5 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e050086..23cafc73c3b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4722,7 +4752,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.49.0 [text/x-patch] v20250625-0004-support-of-session-variables-for-psql.patch (22.7K, 12-v20250625-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 9e1294dc01a2dc720db392c0c44c44dd5b66f256 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 84298bc374e..25c373b6417 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26140,6 +26140,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 95f4cac2467..fb361ce034c 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 9fcd2db8326..cf2dea845c9 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1249,6 +1249,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dd25d2fe7b8..a18b99c7b0b 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index a2e009ab9be..3b821b9bc55 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -262,6 +262,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 908eef97c6e..1579748ed75 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -970,6 +970,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1318,6 +1325,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1883,7 +1891,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2592,6 +2600,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3175,7 +3186,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -3986,6 +3997,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4263,6 +4281,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4464,7 +4488,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4472,6 +4498,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4486,7 +4513,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4494,7 +4522,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4530,6 +4559,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4832,7 +4863,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5321,6 +5352,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5da9959942c..f72acdd84bd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6696,6 +6696,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..d9b02ee99d5 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..8fd75d53afe 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.49.0 [text/x-patch] v20250625-0006-session-variable-fences-parsing.patch (32.5K, 13-v20250625-0006-session-variable-fences-parsing.patch) download | inline diff: From 903022190b91c591cfbe77bd2e672a580b393b5d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 01a9731ce28..6d7c8c56c90 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -524,7 +524,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15637,6 +15637,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17012,6 +17025,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8abe3f0d409..4654574c06c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bdab70f090b..9646f6b5230 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3191,6 +3191,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.49.0 [text/x-patch] v20250625-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 14-v20250625-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 4e79190530f72ef59362d9a7eb3550da9d5611b5 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 197c1295d93..443869725d2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index db944ec2230..0ab8e655387 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -370,6 +370,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5512,6 +5513,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11560,6 +11743,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16014,6 +16200,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19752,6 +19941,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aef..1c722e48de5 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -735,6 +736,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -818,5 +832,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f1..b34f3418f64 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -77,6 +77,7 @@ enum dbObjectTypePriorities PRIO_DUMMY_TYPE, PRIO_ATTRDEF, PRIO_LARGE_OBJECT, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 386e21e0c59..663d31a4557 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -960,6 +960,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1928,6 +1938,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4236,6 +4263,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4700,6 +4745,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 0f4eecccc23..bdab70f090b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3199,6 +3199,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.49.0 [text/x-patch] v20250625-0003-GRANT-REVOKE-variable.patch (52.7K, 15-v20250625-0003-GRANT-REVOKE-variable.patch) download | inline diff: From e20df02b7db53e6297ff0b52c4d4fc6477e73c95 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index dfe8eeabdf8..55957d742bc 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 224d4fe5a9f..84298bc374e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25612,6 +25612,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25851,8 +25870,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26120,6 +26139,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 92bf8419833..01a9731ce28 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -7964,6 +7964,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8009,6 +8017,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8207,6 +8223,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18068,6 +18085,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18725,6 +18743,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d3d28a263fa..5da9959942c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5466,6 +5466,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.49.0 [text/x-patch] v20250625-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250625-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From d25c97d24ec91f1951fc70dedf9fd5970587098e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968b..9e0f648b74f 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 074ddb6b9cd..a9424b1b267 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6910,6 +6911,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6973,6 +6975,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5..8f8b7d09f32 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3373,6 +3374,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 50f53159d58..92bf8419833 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1591,6 +1593,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5265,6 +5268,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7056,6 +7087,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -9963,6 +9995,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10324,6 +10374,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10605,6 +10673,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -17991,6 +18067,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18647,6 +18724,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf..b6cbe354c51 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4091,6 +4092,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4159,6 +4161,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4172,6 +4183,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d58016..782b022da9c 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1389,6 +1391,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2341,6 +2347,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2649,6 +2658,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3225,6 +3237,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3759,6 +3775,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ba12678d1cb..8abe3f0d409 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3520,6 +3521,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e0e831b797f..0f4eecccc23 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -563,6 +563,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.49.0 [text/x-patch] v20250625-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250625-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From cfebc12b52e104dada2df549f729ad9f8a9397e6 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index fa86c569dc4..dfe8eeabdf8 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9773,4 +9778,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 32d6e718adc..e0e831b797f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -910,6 +910,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -969,6 +970,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.49.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-07-11 19:55 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-07-11 19:55 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi just fresh rebase Regards Pavel Attachments: [text/x-patch] 0015-plpgsql-tests.patch (11.3K, 3-0015-plpgsql-tests.patch) download | inline diff: From 5b5818c0e6262060f50ff3e3dfc1a5380db3dc96 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.50.0 [text/x-patch] 0013-DISCARD-VARIABLES.patch (10.1K, 4-0013-DISCARD-VARIABLES.patch) download | inline diff: From 227713646211e067818fdbfeb11ee7f1ec8d0025 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5f6bc0f8a99..46e186f7137 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2090,7 +2090,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index d4e7d83373e..5dd80848f73 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2951,6 +2951,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b994a64ec9a..d55db588a41 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4198,7 +4198,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.50.0 [text/x-patch] 0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 5-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 1eeabbf2c30b085ac7ca0b2cb24dd2afa16310f8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.50.0 [text/x-patch] 0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 6-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From b90724593f651c5ec4a84746a7e27507a4745735 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 5b95bb0d8e5..5a47b5c04fe 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->partition_directory = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -583,6 +597,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 229547b1778..5f6bc0f8a99 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12927,6 +12928,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -18002,6 +18035,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18616,6 +18650,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index c5849c03117..d4e7d83373e 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2196,6 +2202,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2394,6 +2404,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3279,6 +3293,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 5f30d712de9..b994a64ec9a 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1249,8 +1249,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4767,6 +4767,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ec5685df779..4b0c625619f 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 77f864804ad..c173c0af761 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1549,6 +1549,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.50.0 [text/x-patch] 0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 7-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 39a3e1d1dd6d8c2714bb7f99f3e686f419416ccc Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 249ebb73b40..0f3047b5c09 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12595,4 +12595,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.50.0 [text/x-patch] 0010-svariableReceiver.patch (8.9K, 8-0010-svariableReceiver.patch) download | inline diff: From 35eddf8c663a41a6faa213edac6ce4086f18770d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4394dc3f16b..77f864804ad 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2651,6 +2651,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.0 [text/x-patch] 0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 9-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 0fe799cdc7a726007670673d1dee58c26b34652d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 549aedcfa99..5b95bb0d8e5 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -342,6 +342,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -579,6 +580,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -757,6 +761,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b57..1ebc808a5e6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1585,6 +1585,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index f45131c34c5..7bbd52d5630 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -936,6 +937,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2391,6 +2399,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2517,6 +2526,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4729,7 +4759,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595d..ec5685df779 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -179,6 +179,9 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -529,6 +532,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.50.0 [text/x-patch] 0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 10-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From fdf66e827598f2334d938945f49600f01851e961 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 91652376b45..45ab169ce28 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2649,6 +2649,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.0 [text/x-patch] 0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 11-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From f2c65bd17395863522ac71297b79e1fafffff696 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e107d6e5f81..630629f34b6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -649,6 +649,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -708,6 +718,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 1c67fdff564..5a9bfd4dc8b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 45ab169ce28..4394dc3f16b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2708,6 +2708,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.50.0 [text/x-patch] 0006-session-variable-fences-parsing.patch (32.5K, 12-0006-session-variable-fences-parsing.patch) download | inline diff: From 390294d153c4b8faa80b59d99f98b7ba0e17781d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 2d09c0ee741..229547b1778 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -524,7 +524,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15702,6 +15702,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17077,6 +17090,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 32b86f87112..91652376b45 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3198,6 +3198,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.50.0 [text/x-patch] 0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From ac31a60bdec33aac395c6ec9531c91770f5a5dee Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 197c1295d93..443869725d2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3179,6 +3179,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3742,6 +3750,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 1937997ea67..99e06e28acd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -372,6 +372,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5514,6 +5515,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11578,6 +11761,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16032,6 +16218,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -19820,6 +20009,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 39eef1d6617..7b90acd54ac 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -736,6 +737,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -819,5 +833,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 538e7dcb493..efed27357be 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1533,6 +1535,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 2485d8f360e..129964e59f2 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -959,6 +959,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1957,6 +1967,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4265,6 +4292,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4729,6 +4774,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 27df7d3ed67..32b86f87112 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3206,6 +3206,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.50.0 [text/x-patch] 0004-support-of-session-variables-for-psql.patch (22.7K, 14-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 67b7cf084af97481bdfe2749976c1033f13d59d9 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 1b2f056dc4d..6bbddf636e1 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26140,6 +26140,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 95f4cac2467..fb361ce034c 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0a55901b14e..5ea6b5e0bb1 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1249,6 +1249,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dd25d2fe7b8..a18b99c7b0b 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index a2e009ab9be..3b821b9bc55 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -262,6 +262,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6872653c6c8..5f30d712de9 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1339,6 +1346,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1904,7 +1912,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2613,6 +2621,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3219,7 +3230,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4031,6 +4042,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4308,6 +4326,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4509,7 +4533,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4517,6 +4543,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4531,7 +4558,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4539,7 +4567,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4575,6 +4604,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4897,7 +4928,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5386,6 +5417,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 64ea05b1d84..249ebb73b40 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..d9b02ee99d5 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..8fd75d53afe 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.50.0 [text/x-patch] 0003-GRANT-REVOKE-variable.patch (52.7K, 15-0003-GRANT-REVOKE-variable.patch) download | inline diff: From f3e68f4b735da0c97ecc1f366183e30538398d4f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index b9d3386be0d..845a92129c8 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 6b327d4fd81..1b2f056dc4d 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25612,6 +25612,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25851,8 +25870,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26120,6 +26139,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5e93fc0a056..2d09c0ee741 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8029,6 +8029,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8074,6 +8082,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8272,6 +8288,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18133,6 +18150,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18790,6 +18808,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3..07874d4ba39 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -851,6 +854,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -948,6 +955,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5016,6 +5026,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1fc19146f46..64ea05b1d84 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 0f75bfaf5ae..1c67fdff564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.50.0 [text/x-patch] 0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From ecb7bdd76bc2dc8b462c0e1b771630adf15dd537 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb811520c29..928402bebf3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 26d985193ae..b76cfe62d0c 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3381,6 +3382,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 73345bb3c70..5e93fc0a056 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1591,6 +1593,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5278,6 +5281,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7089,6 +7120,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10028,6 +10060,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10389,6 +10439,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10670,6 +10738,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18056,6 +18132,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18712,6 +18789,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4c1faf5575c..c5849c03117 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1379,6 +1381,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2331,6 +2337,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2639,6 +2648,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3215,6 +3227,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3749,6 +3765,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf..0f75bfaf5ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index de40b01d5ca..27df7d3ed67 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -563,6 +563,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.50.0 [text/x-patch] 0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 50d4d1e4f183b3a4d967bf299ec5ac02f1752aa8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index aa5b8772436..b9d3386be0d 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9773,4 +9778,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 83192038571..de40b01d5ca 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -912,6 +912,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -971,6 +972,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.50.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-07-22 05:53 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-07-22 05:53 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi rebase after e2debb6 Regards Pavel Attachments: [text/x-patch] v20250722-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 3-v20250722-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 98bfc336cd1e352d74e80b9c7d7fc81d47993973 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.50.1 [text/x-patch] v20250722-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 4-v20250722-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 29632391160bb81a36e707ab3cf4ac90e16e2f18 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index cd6b3c27ade..b593760af35 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -345,6 +345,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -584,6 +598,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 229547b1778..5f6bc0f8a99 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12927,6 +12928,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -18002,6 +18035,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18616,6 +18650,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index c5849c03117..d4e7d83373e 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2196,6 +2202,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2394,6 +2404,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3279,6 +3293,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ecd7f2499a7..529ec8948cb 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1249,8 +1249,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4771,6 +4771,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 576bcd36b4b..e30bbcc5fc6 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 316eab1fcc2..4e9800b4149 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.50.1 [text/x-patch] v20250722-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 5-v20250722-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 01657c5c981d1019ab8e8bdb5cb54408781228df Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 249ebb73b40..0f3047b5c09 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12595,4 +12595,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.50.1 [text/x-patch] v20250722-0013-DISCARD-VARIABLES.patch (10.1K, 6-v20250722-0013-DISCARD-VARIABLES.patch) download | inline diff: From 5d71f88235c640c0245b13d5c9069ec64b7f7a18 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5f6bc0f8a99..46e186f7137 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2090,7 +2090,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index d4e7d83373e..5dd80848f73 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2951,6 +2951,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 529ec8948cb..b011158c54b 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4198,7 +4198,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.50.1 [text/x-patch] v20250722-0015-plpgsql-tests.patch (11.3K, 7-v20250722-0015-plpgsql-tests.patch) download | inline diff: From 12e037e3f13c22d155c81c4aa8aaeb3dc7d825f4 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.50.1 [text/x-patch] v20250722-0010-svariableReceiver.patch (8.9K, 8-v20250722-0010-svariableReceiver.patch) download | inline diff: From 9b19301157cd3709fc9d89f9c44f07747b64da69 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e1e341c7c04..316eab1fcc2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2652,6 +2652,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250722-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250722-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 55bd766cc0e20721f5f4950b884832247f64ce36 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e107d6e5f81..630629f34b6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -649,6 +649,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -708,6 +718,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e3c4ecde9e3..e1e341c7c04 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2709,6 +2709,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.50.1 [text/x-patch] v20250722-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 10-v20250722-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 15487b56bcfe4f27623d4a4e436633e6fdc96acc Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 15f513b4969..e3c4ecde9e3 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2650,6 +2650,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250722-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 11-v20250722-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 20210c6676cc9db3533974809b4fd97f2716610f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index c989e72cac5..cd6b3c27ade 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -343,6 +343,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -580,6 +581,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -762,6 +766,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cd..6df76d87b72 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index e5dd15098f6..576bcd36b4b 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.50.1 [text/x-patch] v20250722-0006-session-variable-fences-parsing.patch (32.5K, 12-v20250722-0006-session-variable-fences-parsing.patch) download | inline diff: From 36ac863d63d61c2feb7adc7726a16d2a47e8d4ec Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 2d09c0ee741..229547b1778 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -524,7 +524,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15702,6 +15702,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17077,6 +17090,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d1457d079f0..15f513b4969 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3199,6 +3199,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.50.1 [text/x-patch] v20250722-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250722-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From b3965bd7a7e8bdd58d41115297a860285de3e228 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 30e0da31aa3..aa27568bf8e 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3194,6 +3194,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3757,6 +3765,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index ede10e5291e..0832f61368d 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5580,6 +5581,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11682,6 +11865,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16188,6 +16374,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20001,6 +20190,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 2370c98d192..0f7841b18f7 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -738,6 +739,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -821,5 +835,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index f99a0797ea7..aa288790636 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1536,6 +1538,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 6c7ec80e271..f54fd7efd5a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -959,6 +959,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1959,6 +1969,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4293,6 +4320,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4757,6 +4802,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f817b73280f..d1457d079f0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3207,6 +3207,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.50.1 [text/x-patch] v20250722-0004-support-of-session-variables-for-psql.patch (22.7K, 14-v20250722-0004-support-of-session-variables-for-psql.patch) download | inline diff: From c5360d22e964cae8efdf86b9f2b3645ea1868162 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 4872ef08b5f..5fb326999c3 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26140,6 +26140,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 4f7b11175c6..098c36e911f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0e00d73487c..63aff4fbedc 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1249,6 +1249,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index dd25d2fe7b8..a18b99c7b0b 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 8c62729a0d1..13a0e3be9ac 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -262,6 +262,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 37524364290..ecd7f2499a7 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1339,6 +1346,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1904,7 +1912,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2613,6 +2621,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3219,7 +3230,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4031,6 +4042,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4308,6 +4326,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4509,7 +4533,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4517,6 +4543,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4531,7 +4558,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4539,7 +4567,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4575,6 +4604,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4901,7 +4932,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5390,6 +5421,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 64ea05b1d84..249ebb73b40 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..d9b02ee99d5 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..8fd75d53afe 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.50.1 [text/x-patch] v20250722-0003-GRANT-REVOKE-variable.patch (52.7K, 15-v20250722-0003-GRANT-REVOKE-variable.patch) download | inline diff: From c8aabf61515b2155819a2ac57c3410244d972aea Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 232e2f09352..a5119785eae 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index f5a0e0954a1..4872ef08b5f 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25612,6 +25612,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25851,8 +25870,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26120,6 +26139,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5e93fc0a056..2d09c0ee741 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8029,6 +8029,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8074,6 +8082,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8272,6 +8288,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18133,6 +18150,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18790,6 +18808,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 1213f9106d5..dc412010c20 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -868,6 +871,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -965,6 +972,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5033,6 +5043,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1fc19146f46..64ea05b1d84 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.50.1 [text/x-patch] v20250722-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 16-v20250722-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 6a2e11d1391bd25cc6c1e43862afb1c63f66b129 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 0d23bc1b122..232e2f09352 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9773,4 +9778,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index cd897467088..6ad6b4bb7ec 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.50.1 [text/x-patch] v20250722-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 17-v20250722-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 4c5d2a12a99e526bc2e3e3a365cad92c46b701b4 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb811520c29..928402bebf3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 26d985193ae..b76cfe62d0c 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3381,6 +3382,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 73345bb3c70..5e93fc0a056 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1591,6 +1593,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5278,6 +5281,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7089,6 +7120,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10028,6 +10060,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10389,6 +10439,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10670,6 +10738,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18056,6 +18132,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18712,6 +18789,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4c1faf5575c..c5849c03117 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1379,6 +1381,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2331,6 +2337,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2639,6 +2648,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3215,6 +3227,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3749,6 +3765,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6ad6b4bb7ec..f817b73280f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.50.1 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-07-23 15:25 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-07-23 15:25 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi fresh rebase - testing farms reports failure Regards Pavel Attachments: [text/x-patch] v20250723-0015-plpgsql-tests.patch (11.3K, 3-v20250723-0015-plpgsql-tests.patch) download | inline diff: From e741fcbd99595f98c981c765517814a88a01a734 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.50.1 [text/x-patch] v20250723-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 4-v20250723-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From fc5b50afa54b6b49168242e216d62ffd90aa6926 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index cd6b3c27ade..b593760af35 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -345,6 +345,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -584,6 +598,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 229547b1778..5f6bc0f8a99 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12927,6 +12928,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -18002,6 +18035,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18616,6 +18650,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index c5849c03117..d4e7d83373e 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2196,6 +2202,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2394,6 +2404,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3279,6 +3293,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ca92be9a6ae..59469c52abe 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2030,6 +2030,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b18b63d7f15..345c10b85f1 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1249,8 +1249,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4773,6 +4773,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 576bcd36b4b..e30bbcc5fc6 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3622699cfc9..10761cf9105 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -134,6 +134,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c430c78f17f..d4345c450d6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.50.1 [text/x-patch] v20250723-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 5-v20250723-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From e83a066bb38e02219d03ce3d4ec9a1c4619b9d1c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index bba27327e44..25d901a03f2 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12599,4 +12599,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.50.1 [text/x-patch] v20250723-0013-DISCARD-VARIABLES.patch (10.1K, 6-v20250723-0013-DISCARD-VARIABLES.patch) download | inline diff: From 45144705bd242c05a8bbcadb50c1e2dc4363c082 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5f6bc0f8a99..46e186f7137 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2090,7 +2090,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index d4e7d83373e..5dd80848f73 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2951,6 +2951,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 345c10b85f1..8ef3efe2437 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4200,7 +4200,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.50.1 [text/x-patch] v20250723-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 7-v20250723-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 988d73821db2a19aee0dedf66305db2be3d5b290 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.50.1 [text/x-patch] v20250723-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 8-v20250723-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 19565921c89f4f12d7faa974ec99175df2feb505 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 39820b4f545..ca92be9a6ae 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2020,9 +2020,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2030,7 +2033,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2045,6 +2049,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e107d6e5f81..630629f34b6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -649,6 +649,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -708,6 +718,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index eebeb54082f..d4edd2a5e87 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2711,6 +2711,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.50.1 [text/x-patch] v20250723-0010-svariableReceiver.patch (8.9K, 9-v20250723-0010-svariableReceiver.patch) download | inline diff: From 6f919c36b1300904e70aaba9a0226c6a9074badc Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d4edd2a5e87..c430c78f17f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2654,6 +2654,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250723-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250723-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 6c7134b1417b653bd0cb77caf9481e888ccc31bc Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index c989e72cac5..cd6b3c27ade 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -343,6 +343,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -580,6 +581,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -762,6 +766,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cd..6df76d87b72 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984..39820b4f545 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2173,7 +2175,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index e5dd15098f6..576bcd36b4b 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4f59e30d62d..3622699cfc9 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.50.1 [text/x-patch] v20250723-0006-session-variable-fences-parsing.patch (32.5K, 11-v20250723-0006-session-variable-fences-parsing.patch) download | inline diff: From dc6b4ad89b1f019e5348f7be2c227c7ae3e2a47e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 2d09c0ee741..229547b1778 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -524,7 +524,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15702,6 +15702,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17077,6 +17090,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3f7a9a6092e..b41647a2a4e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3201,6 +3201,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.50.1 [text/x-patch] v20250723-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 12-v20250723-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From b126f2fac1e1b40e2dc2c02f3ec026999b4f3ab3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index b41647a2a4e..eebeb54082f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2652,6 +2652,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250723-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250723-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From ca97be9acae74b9173a983aaf9ae41ba3add89bd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331..86360bd6d95 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -246,6 +246,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2..76356524af2 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 30e0da31aa3..aa27568bf8e 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3194,6 +3194,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3757,6 +3765,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 6298edb26b5..99051ad00d6 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5594,6 +5595,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11696,6 +11879,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16202,6 +16388,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20015,6 +20204,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 93a4475d51b..ec44ceed8f0 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -739,6 +740,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -822,5 +836,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index f99a0797ea7..aa288790636 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1536,6 +1538,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 6c7ec80e271..f54fd7efd5a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -959,6 +959,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1959,6 +1969,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4293,6 +4320,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4757,6 +4802,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ff1fb734642..3f7a9a6092e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3209,6 +3209,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.50.1 [text/x-patch] v20250723-0003-GRANT-REVOKE-variable.patch (52.7K, 14-v20250723-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 586e5100b1b562c2b77d6ca9c49ae782c7fcbf20 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 18:58:51 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func.sgml | 24 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 916 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index c95a24a2be0..8e356085a5e 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index de5b5929ee0..381e01c04d8 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25612,6 +25612,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -25851,8 +25870,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> @@ -26120,6 +26139,7 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5e93fc0a056..2d09c0ee741 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8029,6 +8029,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8074,6 +8082,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8272,6 +8288,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18133,6 +18150,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18790,6 +18808,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 1213f9106d5..dc412010c20 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -868,6 +871,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -965,6 +972,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5033,6 +5043,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3ee8fed7e53..cd55f50c944 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.50.1 [text/x-patch] v20250723-0004-support-of-session-variables-for-psql.patch (22.7K, 15-v20250723-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 4965b907219734ad81c443c8d57bf6eb6a12d619 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:16:16 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func.sgml | 12 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 258 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 381e01c04d8..e46fa3e7573 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -26140,6 +26140,18 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 4f7b11175c6..098c36e911f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0e00d73487c..63aff4fbedc 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1249,6 +1249,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 7a06af48842..4e89624f255 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 8c62729a0d1..13a0e3be9ac 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -262,6 +262,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index dbc586c5bc3..b18b63d7f15 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1339,6 +1346,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1904,7 +1912,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2614,6 +2622,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3220,7 +3231,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4033,6 +4044,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4310,6 +4328,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4511,7 +4535,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4519,6 +4545,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4533,7 +4560,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4541,7 +4569,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4577,6 +4606,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4903,7 +4934,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5392,6 +5423,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index cd55f50c944..bba27327e44 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..d9b02ee99d5 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..8fd75d53afe 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.50.1 [text/x-patch] v20250723-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250723-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 643fd0c44b22d33dae69e0cb849fc73094a8913f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb811520c29..928402bebf3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 26d985193ae..b76cfe62d0c 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3381,6 +3382,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 73345bb3c70..5e93fc0a056 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1591,6 +1593,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5278,6 +5281,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7089,6 +7120,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10028,6 +10060,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10389,6 +10439,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10670,6 +10738,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18056,6 +18132,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18712,6 +18789,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4c1faf5575c..c5849c03117 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1379,6 +1381,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2331,6 +2337,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2639,6 +2648,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3215,6 +3227,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3749,6 +3765,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6afd1d7add8..ff1fb734642 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.50.1 [text/x-patch] v20250723-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250723-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From e635158c51e8173190470ad293ffa7626c577717 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 97f547b3cc4..c95a24a2be0 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9784,4 +9789,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8656419cb6..6afd1d7add8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.50.1 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-08-05 05:30 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-08-05 05:30 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi fresh rebase after 4e23c9ef65accde7eb3e56aa28d50ae5cf79b64b Regards Pavel Attachments: [text/x-patch] v20250805-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 3-v20250805-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 977cdfd38324ecf663ad80b29257c05a140c5b4a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.50.1 [text/x-patch] v20250805-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 4-v20250805-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From 806ac74ca3774550ef45b5f149ebe275a00860d2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a71b9059822..d0399ec13fa 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12599,4 +12599,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.50.1 [text/x-patch] v20250805-0015-plpgsql-tests.patch (11.3K, 5-v20250805-0015-plpgsql-tests.patch) download | inline diff: From 641360d9e5e184bdee7b1898a3d0c9f9eec2a1cc Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.50.1 [text/x-patch] v20250805-0013-DISCARD-VARIABLES.patch (10.1K, 6-v20250805-0013-DISCARD-VARIABLES.patch) download | inline diff: From c7087913e08b7b365c5ddce8291beadcc593a88d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a80b0358d67..3cc2ea978e1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 3cd1b0871f0..7a0d6ea1d00 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ed05416c2ab..07f72fca8b2 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4206,7 +4206,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.50.1 [text/x-patch] v20250805-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 7-v20250805-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 1cf78c6130ae615d245f82b39bdd90b72a646bd8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 223742b920f..685ba66fd34 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -235,13 +235,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 5b59f2034fd..6b82c77e42c 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -345,6 +345,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -585,6 +599,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 52a242ccc7f..a80b0358d67 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12915,6 +12916,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17990,6 +18023,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18604,6 +18638,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 97ad9596561..3cd1b0871f0 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 682a7c8d28d..ed05416c2ab 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1252,8 +1252,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4779,6 +4779,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index cfd2b24f32a..78b096f2840 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 35971e3a338..9d484315f70 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 594f9e2490d..3e665d2c4be 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.50.1 [text/x-patch] v20250805-0010-svariableReceiver.patch (8.9K, 8-v20250805-0010-svariableReceiver.patch) download | inline diff: From 7d327c7db8719a66a5c881f202f6a82b9a45c623 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ccfa6db839d..594f9e2490d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2655,6 +2655,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250805-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250805-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 2b2a017dc6ac9c4239e0b2e50e4edc8dcec09331 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c..223742b920f 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -197,6 +199,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e107d6e5f81..630629f34b6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -649,6 +649,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -708,6 +718,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index eac482d7bec..ccfa6db839d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2712,6 +2712,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.50.1 [text/x-patch] v20250805-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250805-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 1f76f99f370f548cc65e950102459b27161f1e32 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index d59d6e4c6a0..5b59f2034fd 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -343,6 +343,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -581,6 +582,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -763,6 +767,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cd..6df76d87b72 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ad2726f026f..cfd2b24f32a 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 29d7732d6a0..35971e3a338 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.50.1 [text/x-patch] v20250805-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250805-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From abb6ce7aa28e2fc749cefcf262394f58abfc41ba Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d1e351b046e..eac482d7bec 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2653,6 +2653,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250805-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250805-0006-session-variable-fences-parsing.patch) download | inline diff: From 89cf70d87040a556987fd0aae1930285b69a4e00 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index abc365c0cfa..52a242ccc7f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15690,6 +15690,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17065,6 +17078,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fdb7eaef75a..2c1488a4ffa 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8962d4823d2..d1e351b046e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3202,6 +3202,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.50.1 [text/x-patch] v20250805-0004-support-of-session-variables-for-psql.patch (22.8K, 13-v20250805-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 82016e3842f5796f5c7fdaf980f6b8d593e7256c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index e938e1850df..11a952cd08e 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 4f7b11175c6..098c36e911f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0e00d73487c..63aff4fbedc 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1249,6 +1249,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 7a06af48842..4e89624f255 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 8c62729a0d1..13a0e3be9ac 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -262,6 +262,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 1f2ca946fc5..682a7c8d28d 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2620,6 +2628,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3226,7 +3237,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4039,6 +4050,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4316,6 +4334,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4517,7 +4541,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4525,6 +4551,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4539,7 +4566,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4547,7 +4575,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4583,6 +4612,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4909,7 +4940,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5398,6 +5429,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7fa5f7366c1..a71b9059822 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..d9b02ee99d5 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6023,6 +6023,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6244,6 +6268,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6478,6 +6508,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6647,6 +6683,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6780,6 +6822,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6827,6 +6875,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..8fd75d53afe 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1643,6 +1643,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1754,6 +1767,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1795,6 +1811,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1835,6 +1853,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1859,6 +1878,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1884,6 +1904,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.50.1 [text/x-patch] v20250805-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 14-v20250805-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From c9a3be15274c6fb2ee294a646973ea5e3ad4e487 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 64 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 297 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b2..cbca7c343a1 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -514,6 +514,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index 4ebef1e8644..fd293dd2863 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index dce88f040ac..2f985c737fa 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3188,6 +3188,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3751,6 +3759,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index f3a353a61a5..da1904e4301 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5589,6 +5590,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11721,6 +11904,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16227,6 +16413,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20040,6 +20229,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index dde85ed156c..12bebf4a988 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -744,6 +745,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -828,5 +842,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index a02da3e9652..d2848faf0d2 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1736,6 +1738,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index a86b38466de..094f0836d95 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -952,6 +952,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1952,6 +1962,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4286,6 +4313,24 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4750,6 +4795,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 67056651f7f..8962d4823d2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3210,6 +3210,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.50.1 [text/x-patch] v20250805-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 15-v20250805-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From a0f8c95e3a7b9a2a36531ed6aee0adf7a9de2e21 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04..9413b7619c5 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e8..c6fd1fa074f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1c..c72a4adb07b 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e29..1839c1f82c5 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb811520c29..928402bebf3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 26d985193ae..b76cfe62d0c 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3381,6 +3382,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index db43034b9db..3140c772178 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5307,6 +5310,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7118,6 +7149,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10053,6 +10085,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10414,6 +10464,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10695,6 +10763,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18044,6 +18120,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18700,6 +18777,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4f4191b0ea6..97ad9596561 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d..fdb7eaef75a 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index be081e96413..67056651f7f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.50.1 [text/x-patch] v20250805-0003-GRANT-REVOKE-variable.patch (52.5K, 16-v20250805-0003-GRANT-REVOKE-variable.patch) download | inline diff: From c5431647cfc983f32c451947ee806b137c5c36d7 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 915 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index b33f3f06d71..b1815f8d2ab 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index b507bfaf64b..e938e1850df 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 9413b7619c5..444892ad6ba 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3140c772178..abc365c0cfa 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8058,6 +8058,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8103,6 +8111,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8301,6 +8317,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18121,6 +18138,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18778,6 +18796,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 1213f9106d5..dc412010c20 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -868,6 +871,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -965,6 +972,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5033,6 +5043,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..7fa5f7366c1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.50.1 [text/x-patch] v20250805-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250805-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From e528ccd6ea02cb8b2c52a8ef3557e8ffb7014cb2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index da8a7882580..b33f3f06d71 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9785,4 +9790,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e6f2e93b2d6..be081e96413 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.50.1 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-08-13 19:24 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-08-13 19:24 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi fix regress tests after 71ea0d6795438f95f4ee6e35867058c44b270d51 Regards Pavel Attachments: [text/x-patch] v20250813-0015-plpgsql-tests.patch (11.3K, 3-v20250813-0015-plpgsql-tests.patch) download | inline diff: From da3da6c6f18b5a06da81145ff58da588cdd54dd7 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.50.1 [text/x-patch] v20250813-0013-DISCARD-VARIABLES.patch (10.1K, 4-v20250813-0013-DISCARD-VARIABLES.patch) download | inline diff: From 020682f1e7f5d39ceda97785d1f89fce9aa4b780 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a80b0358d67..3cc2ea978e1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 3cd1b0871f0..7a0d6ea1d00 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 1e7c0392c46..4ec384f3a88 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4206,7 +4206,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.50.1 [text/x-patch] v20250813-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 5-v20250813-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 8173c667fcf024ee5fff1bc5ef2c8a0e6ab87dd3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7..8df901fc79c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -116,3 +116,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.50.1 [text/x-patch] v20250813-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 6-v20250813-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From d637125493c1134c78eef5114c3c8e1a36b3b3df Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a71b9059822..d0399ec13fa 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12599,4 +12599,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.50.1 [text/x-patch] v20250813-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 7-v20250813-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 0df29fe2ef785aa26bbec4070b6be85802870584 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 453ab94a8df..0f39f576794 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 1d98fbd6d50..ad5c69c0a09 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -346,6 +346,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -586,6 +600,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6c86ee1ad64..d47a726f2df 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2224,6 +2224,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3709,7 +3730,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3811,9 +3832,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3836,6 +3858,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 52a242ccc7f..a80b0358d67 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12915,6 +12916,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17990,6 +18023,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18604,6 +18638,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 97ad9596561..3cd1b0871f0 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index f37144e437e..1e7c0392c46 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1252,8 +1252,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4779,6 +4779,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index cfd2b24f32a..78b096f2840 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 35971e3a338..9d484315f70 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 594f9e2490d..3e665d2c4be 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.50.1 [text/x-patch] v20250813-0010-svariableReceiver.patch (8.9K, 8-v20250813-0010-svariableReceiver.patch) download | inline diff: From 49145ab5ef34502e3de022c01525d01bf9a7ed3a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ccfa6db839d..594f9e2490d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2655,6 +2655,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250813-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250813-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 9f9551ab4ae3978edfe983ab17566f86862a700c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index b8b9d2a85f7..453ab94a8df 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +198,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e107d6e5f81..630629f34b6 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -649,6 +649,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -708,6 +718,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index eac482d7bec..ccfa6db839d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2712,6 +2712,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.50.1 [text/x-patch] v20250813-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250813-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 12d3e4dbb005e7306c60965c91911097c7e9e1bb Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 5ba0d22befd..1d98fbd6d50 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -344,6 +344,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -582,6 +583,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -764,6 +768,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c3..6c86ee1ad64 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1316,6 +1319,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2016,8 +2063,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2107,6 +2155,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2116,6 +2171,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2134,6 +2193,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2195,7 +2288,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2217,7 +2313,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3610,6 +3707,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cd..6df76d87b72 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ad2726f026f..cfd2b24f32a 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 29d7732d6a0..35971e3a338 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.50.1 [text/x-patch] v20250813-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250813-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 6d0339dec09238b13fd993ce06f2a31fe9380273 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d1e351b046e..eac482d7bec 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2653,6 +2653,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.50.1 [text/x-patch] v20250813-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250813-0006-session-variable-fences-parsing.patch) download | inline diff: From a372f863e451301fb7b95cc1e12cc94c1266f0c0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index b450471e063..cdbe9ff4fcb 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index abc365c0cfa..52a242ccc7f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15690,6 +15690,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17065,6 +17078,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index d66276801c6..134b5a6b092 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -77,6 +78,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -106,7 +108,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -370,6 +374,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -903,6 +911,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3122,6 +3259,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5..cf01bff65a9 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 34504bf3d6b..7f81b14d61b 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3919,3 +3919,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 87e1fe636b9..f5160de67d9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 8962d4823d2..d1e351b046e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3202,6 +3202,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.50.1 [text/x-patch] v20250813-0004-support-of-session-variables-for-psql.patch (22.8K, 13-v20250813-0004-support-of-session-variables-for-psql.patch) download | inline diff: From f52fb2d0ec54e8358da516acd6714506b29be8d3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4abbe733f45..b450471e063 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5292,3 +5292,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 7a06af48842..4e89624f255 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8b10f2313f3..f37144e437e 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2620,6 +2628,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3226,7 +3237,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4039,6 +4050,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4316,6 +4334,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4517,7 +4541,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4525,6 +4551,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4539,7 +4566,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4547,7 +4575,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4583,6 +4612,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4909,7 +4940,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5398,6 +5429,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7fa5f7366c1..a71b9059822 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.50.1 [text/x-patch] v20250813-0003-GRANT-REVOKE-variable.patch (52.5K, 14-v20250813-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 44ec9c34afd987a0ed9e1b109f164e098548e2f8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 915 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index b33f3f06d71..b1815f8d2ab 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..a57f7665054 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 7075c86378a..e333ad22e25 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -291,6 +292,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -535,6 +539,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -640,6 +648,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -760,6 +771,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -846,6 +869,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1009,6 +1058,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1209,6 +1262,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1456,6 +1514,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1516,6 +1577,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2749,6 +2813,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2771,7 +2838,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3012,6 +3078,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4275,6 +4343,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index ee37fd85973..6182a5771ac 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2030,17 +2030,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3922,6 +3926,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5852,6 +5866,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3140c772178..abc365c0cfa 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8058,6 +8058,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8103,6 +8111,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8301,6 +8317,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18121,6 +18138,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18778,6 +18796,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 1213f9106d5..dc412010c20 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" @@ -129,6 +130,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -868,6 +871,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -965,6 +972,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5033,6 +5043,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..7fa5f7366c1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.50.1 [text/x-patch] v20250813-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 15-v20250813-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 61850e04a105ec672631df95548f8dcbdab55d56 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 63 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 296 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 8945bdd42c5..af10f9e04da 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -552,6 +552,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index d9041dad720..dac067bdc4c 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 3c3acbaccdb..48b2f476b77 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3207,6 +3207,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3780,6 +3788,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index fc7a6639163..76ad84d97a3 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5613,6 +5614,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11745,6 +11928,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16251,6 +16437,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20064,6 +20253,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index dde85ed156c..12bebf4a988 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -744,6 +745,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -828,5 +842,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index a02da3e9652..d2848faf0d2 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1736,6 +1738,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index e7a2d64f741..84153db459d 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -962,6 +962,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1962,6 +1972,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4315,6 +4342,23 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4779,6 +4823,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 67056651f7f..8962d4823d2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3210,6 +3210,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.50.1 [text/x-patch] v20250813-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250813-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 31d7307a6dae4268c4c95e6b4e1a60b2215f01c0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e9..83b13f98f28 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1708,6 +1708,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc91..7075c86378a 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2878,6 +2879,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef..4abbe733f45 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" @@ -225,6 +226,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -986,6 +988,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3289,6 +3369,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 0102c9984e7..ee37fd85973 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -831,6 +846,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -856,6 +874,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1127,6 +1146,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2102,6 +2124,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2296,6 +2336,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2467,6 +2508,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3496,6 +3538,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4670,6 +4738,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6020,6 +6092,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 32e544da28a..41a61cc7c5f 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/dbcommands.h" #include "commands/defrem.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index cb75e11fced..aaf61a6f61c 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 631fb0525f1..5e0ce55daa1 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index c6dd2e020da..5d99be3e623 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 26d985193ae..b76cfe62d0c 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3381,6 +3382,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index db43034b9db..3140c772178 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5307,6 +5310,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7118,6 +7149,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10053,6 +10085,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10414,6 +10464,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10695,6 +10763,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18044,6 +18120,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18700,6 +18777,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4f4191b0ea6..97ad9596561 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 032bb6222c4..34504bf3d6b 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -39,6 +39,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3854,3 +3855,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b..87e1fe636b9 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index be081e96413..67056651f7f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.50.1 [text/x-patch] v20250813-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250813-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From cde9b15d83991d2bc229ee3346128ca306c35596 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index da8a7882580..b33f3f06d71 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9785,4 +9790,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e6f2e93b2d6..be081e96413 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.50.1 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-08-29 07:03 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-08-29 07:03 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi rebase after 325fc0ab14d11fc87da594857ffbb6636affe7c0 Regards Pavel Attachments: [text/x-patch] v20250829-0015-plpgsql-tests.patch (11.3K, 3-v20250829-0015-plpgsql-tests.patch) download | inline diff: From 09b495fefb9517bc3a47d284fe65771fae45bcff Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 198 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 137 ++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..1dcec78c234 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,198 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..5abf18e3bb3 --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,137 @@ +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.51.0 [text/x-patch] v20250829-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.3K, 4-v20250829-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 1aaa3b7a901a2ef40be3e0bef5e718789290b409 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 10f9b5e2021..0b88c0671a0 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..afcb06599e9 --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name |removed +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, removed FROM pg_session_variables(); +schema|name|removed +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 9f1e997d81b..4763279c89c 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -118,3 +118,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..a38b9761fd8 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, removed FROM pg_session_variables(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..59c2cf8bfa6 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..6b962cf8e5a 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_session_variables() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.51.0 [text/x-patch] v20250829-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 5-v20250829-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 9a64845fc1fadbd9ffee5e7dc23bbe5b383decec Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 453ab94a8df..0f39f576794 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 401f71d0073..ea16431e22b 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -352,6 +352,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -592,6 +606,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 9b8be530b83..8ee6e38869e 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2228,6 +2228,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3713,7 +3734,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3815,9 +3836,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3840,6 +3862,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 95bb0620f39..92715bf8b1f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3362,6 +3372,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 52a242ccc7f..a80b0358d67 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12915,6 +12916,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17990,6 +18023,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18604,6 +18638,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 97ad9596561..3cd1b0871f0 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index f37144e437e..1e7c0392c46 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1252,8 +1252,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4779,6 +4779,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index d33ad33798d..7ba9685cc0e 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 35971e3a338..9d484315f70 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3ae88d9241d..b6bbf4a8e71 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.51.0 [text/x-patch] v20250829-0012-function-pg_session_variables-for-cleaning-tests.patch (4.8K, 6-v20250829-0012-function-pg_session_variables-for-cleaning-tests.patch) download | inline diff: From fafce80b2b884651348c28242b4f8ffd854d5e30 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_session_variables for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..9a9281acd83 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_session_variables - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_session_variables(PG_FUNCTION_ARGS) +{ +#define NUM_PG_SESSION_VARIABLES_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_SESSION_VARIABLES_ATTS]; + bool nulls[NUM_PG_SESSION_VARIABLES_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index a71b9059822..d0399ec13fa 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12599,4 +12599,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'list of used session variables', + proname => 'pg_session_variables', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,removed,can_select,can_update}', + prosrc => 'pg_session_variables' }, ] -- 2.51.0 [text/x-patch] v20250829-0013-DISCARD-VARIABLES.patch (10.1K, 7-v20250829-0013-DISCARD-VARIABLES.patch) download | inline diff: From ccb663d2da82b3c48be2d7403015d38657680577 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 9a9281acd83..10f9b5e2021 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_session_variables(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_session_variables(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a80b0358d67..3cc2ea978e1 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 3cd1b0871f0..7a0d6ea1d00 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 1e7c0392c46..4ec384f3a88 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4206,7 +4206,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..2bf7bd27467 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_session_variables(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_session_variables(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..5a721750446 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_session_variables() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_session_variables(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_session_variables(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.51.0 [text/x-patch] v20250829-0010-svariableReceiver.patch (8.9K, 8-v20250829-0010-svariableReceiver.patch) download | inline diff: From cfa5ab6c55cacc9c33ba0e63506003505c42e71b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f32dcc29023..3ae88d9241d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2656,6 +2656,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250829-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 9-v20250829-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From c4d299d8dad07fb1a73517a74408e152504e21cd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 41bd8353430..401f71d0073 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -350,6 +350,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -588,6 +589,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -770,6 +774,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index d706546f332..9b8be530b83 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1320,6 +1323,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2020,8 +2067,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2111,6 +2159,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2120,6 +2175,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2138,6 +2197,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2199,7 +2292,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2221,7 +2317,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3614,6 +3711,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 6f0b338d2cd..6df76d87b72 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 5543440a33e..926a7b3a096 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -1991,9 +1991,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 4a903d1ec18..d33ad33798d 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 29d7732d6a0..35971e3a338 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.51.0 [text/x-patch] v20250829-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 10-v20250829-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From f33b94c077462d7cbd5f768275bc069a8b6f6772 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index b8b9d2a85f7..453ab94a8df 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +198,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index de782014b2d..67cd0a26764 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -647,6 +647,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -706,6 +716,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f8d95c8e330..f32dcc29023 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2713,6 +2713,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.51.0 [text/x-patch] v20250829-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250829-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From dea9548b0ee194a16c0026bc1cc2c9f0ce42657f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index c37fd5da50b..97cd13957bb 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1717,8 +1717,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d6585c2e6e5..f8d95c8e330 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2654,6 +2654,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250829-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250829-0006-session-variable-fences-parsing.patch) download | inline diff: From 8e71c8dbdd6a59ac4fe21cbcf07c2e9679d49f66 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 78cabf964ef..1d053a35ca0 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3496,6 +3496,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 34f7c17f576..95bb0620f39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2449,6 +2454,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2516,6 +2522,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2992,6 +2999,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index abc365c0cfa..52a242ccc7f 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15690,6 +15690,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17065,6 +17078,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e1979a80c19..d17d0583cd4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -76,6 +77,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -105,7 +107,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -369,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -902,6 +910,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3121,6 +3258,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 905c975d83b..8b5240cd54b 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2033,6 +2033,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 1c4031eea23..b9b0ac55475 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3946,3 +3946,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 5daa308f4eb..49d116fa60a 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2b21c8f8c3f..d6585c2e6e5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3201,6 +3201,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.51.0 [text/x-patch] v20250829-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250829-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 6ec968abcf29742e48cd8cec63f96c07fc2df44d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 63 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 296 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 8945bdd42c5..af10f9e04da 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -552,6 +552,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index d9041dad720..dac067bdc4c 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 3c3acbaccdb..48b2f476b77 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3207,6 +3207,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3780,6 +3788,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index fc7a6639163..76ad84d97a3 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5613,6 +5614,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11745,6 +11928,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16251,6 +16437,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20064,6 +20253,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index dde85ed156c..12bebf4a988 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -744,6 +745,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -828,5 +842,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 2d02456664b..6b3fb9840ae 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1749,6 +1751,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index e7a2d64f741..84153db459d 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -962,6 +962,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1962,6 +1972,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4315,6 +4342,23 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4779,6 +4823,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4694653a91d..2b21c8f8c3f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3209,6 +3209,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.51.0 [text/x-patch] v20250829-0003-GRANT-REVOKE-variable.patch (52.5K, 14-v20250829-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 9d45eb788ed49797104f305bea78e17db4ed8a56 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 915 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index b33f3f06d71..b1815f8d2ab 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..a57f7665054 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 00e3630e0ec..93c10beebe9 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -290,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -534,6 +538,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -639,6 +647,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -773,6 +784,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -859,6 +882,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1022,6 +1071,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1222,6 +1275,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1469,6 +1527,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1529,6 +1590,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2762,6 +2826,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2784,7 +2851,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3025,6 +3091,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4288,6 +4356,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 3817efb1eeb..333be0b2ced 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2029,17 +2029,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3921,6 +3925,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5851,6 +5865,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3140c772178..abc365c0cfa 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8058,6 +8058,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8103,6 +8111,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8301,6 +8317,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18121,6 +18138,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18778,6 +18796,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 7dadaefdfc1..e4522b72830 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "common/hashfn.h" @@ -128,6 +129,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -867,6 +870,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -964,6 +971,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5032,6 +5042,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..7fa5f7366c1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5467,6 +5467,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.51.0 [text/x-patch] v20250829-0004-support-of-session-variables-for-psql.patch (22.8K, 15-v20250829-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 6ba78e3f6520b983888822114e23a9f97bfeab95 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ea49a0bc82d..78cabf964ef 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5291,3 +5291,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 7a06af48842..4e89624f255 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8b10f2313f3..f37144e437e 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2620,6 +2628,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3226,7 +3237,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4039,6 +4050,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4316,6 +4334,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4517,7 +4541,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4525,6 +4551,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4539,7 +4566,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4547,7 +4575,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4583,6 +4612,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4909,7 +4940,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5398,6 +5429,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7fa5f7366c1..a71b9059822 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6697,6 +6697,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.51.0 [text/x-patch] v20250829-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250829-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 2c4899453ba6e3739be0cbf13844989b99bbacf8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8651f0cdb91..c37fd5da50b 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1711,6 +1711,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index cd139bd65a6..00e3630e0ec 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2784,6 +2784,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2891,6 +2892,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 8bd4d6c3d43..ea49a0bc82d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "common/hashfn_unstable.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -224,6 +225,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -985,6 +987,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3288,6 +3368,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 91f3018fd0a..3817efb1eeb 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -635,6 +636,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -830,6 +845,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -855,6 +873,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1126,6 +1145,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2101,6 +2123,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2295,6 +2335,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2466,6 +2507,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3495,6 +3537,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4669,6 +4737,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6019,6 +6091,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 16e3e5c7457..6e3e8813328 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index cb75e11fced..aaf61a6f61c 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 631fb0525f1..5e0ce55daa1 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2149,6 +2149,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2232,6 +2234,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 082a3575d62..2dcd760a4ff 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -52,6 +52,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6909,6 +6910,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6972,6 +6974,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index c6de04819f1..4749f3f7e56 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3391,6 +3392,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index db43034b9db..3140c772178 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5307,6 +5310,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7118,6 +7149,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10053,6 +10085,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10414,6 +10464,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10695,6 +10763,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18044,6 +18120,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18700,6 +18777,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index afcf54169c3..840baeacc8a 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4113,6 +4114,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4181,6 +4183,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4194,6 +4205,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4f4191b0ea6..97ad9596561 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fa7cd7e06a7..1c4031eea23 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -40,6 +40,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3881,3 +3882,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index c65cee4f24c..5daa308f4eb 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3fe07f2ab64..4694653a91d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.51.0 [text/x-patch] v20250829-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250829-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 87dd6030ac1308583284d06100672a9c38e416fd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index da8a7882580..b33f3f06d71 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9785,4 +9790,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a13e8162890..3fe07f2ab64 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.51.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-09-13 09:28 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-09-13 09:28 UTC (permalink / raw) To: Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]>; Jim Jones <[email protected]> Hi minor change (after private talk with Jim Jones) renaming function `pg_session_variables` to `pg_get_session_variables_memory` renaming out parameter of this function `removed` to `dropped` + regress test plpgsql - plpgsql routines has not strong dependency to session variables (same dependency like to other database objects) Regards Pavel Attachments: [text/x-patch] v20250913-0012-function-pg_get_session_variables_memory-for-cleanin.patch (4.9K, 3-v20250913-0012-function-pg_get_session_variables_memory-for-cleanin.patch) download | inline diff: From 94f05982ffb1abf9dd1f4864dce04e28b61290f2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_get_session_variables_memory for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..73ebf0340e3 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_get_session_variables_memory - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_get_session_variables_memory(PG_FUNCTION_ARGS) +{ +#define NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + bool nulls[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 593bd95eb23..cc086f3faf0 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12611,4 +12611,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'internal view of memory entries used by session variables (for debugging)', + proname => 'pg_get_session_variables_memory', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,dropped,can_select,can_update}', + prosrc => 'pg_get_session_variables_memory' }, ] -- 2.51.0 [text/x-patch] v20250913-0013-DISCARD-VARIABLES.patch (10.2K, 4-v20250913-0013-DISCARD-VARIABLES.patch) download | inline diff: From 85fbeb50d73a7fde48ddc0711fda9097c48d841a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 73ebf0340e3..52126a10f62 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 441b6d03af5..f606f392070 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index f0a2438103f..2e0819c6619 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2954,6 +2954,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 41b6a2c8d86..dca77b9dab6 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4208,7 +4208,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..dfbe7900c1c 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..b0aeeb865c2 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.51.0 [text/x-patch] v20250913-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 5-v20250913-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 0bbebfbf31c0484457b547938ebee179ad98d0dd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index ca4e77cd5b4..2b32f1cbda4 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 401f71d0073..ea16431e22b 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -352,6 +352,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -592,6 +606,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 9b8be530b83..8ee6e38869e 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2228,6 +2228,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3713,7 +3734,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3815,9 +3836,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3840,6 +3862,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index fd10c133afc..19c1936e302 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3359,6 +3369,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a9d69f2a1e9..441b6d03af5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12916,6 +12917,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17991,6 +18024,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18605,6 +18639,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25413050601..f0a2438103f 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2199,6 +2205,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2397,6 +2407,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3282,6 +3296,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 928b9dda714..41b6a2c8d86 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1252,8 +1252,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4781,6 +4781,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index d33ad33798d..7ba9685cc0e 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 35971e3a338..9d484315f70 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3ae88d9241d..b6bbf4a8e71 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.51.0 [text/x-patch] v20250913-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.4K, 6-v20250913-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From fd2cc41975407e14d358ef16ffc68d5666479409 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 52126a10f62..cb7aa1a11ed 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..b69dac2df7a --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 5afae33d370..3068ae401db 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -120,3 +120,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..72629321d90 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, dropped FROM pg_get_session_variables_memory(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..3aef7665edc 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..a616ecdc8ef 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.51.0 [text/x-patch] v20250913-0015-plpgsql-tests.patch (13.6K, 7-v20250913-0015-plpgsql-tests.patch) download | inline diff: From b1cf126c40fc46c2aa9a54290544d193b132cd28 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 239 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 168 ++++++++++++ 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ec5ffcb42b2 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,239 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func00_01(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: VARIABLE(this_variable_doesnt_exists) + ^ +QUERY: VARIABLE(this_variable_doesnt_exists) +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_01() line 3 at RAISE +SELECT svartest_plpgsql_func00_02(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists = 10 + ^ +QUERY: LET this_variable_doesnt_exists = 10 +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_02() line 3 at SQL statement +SELECT svartest_plpgsql_func00_03(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists[10] = 'Hello' + ^ +QUERY: LET this_variable_doesnt_exists[10] = 'Hello' +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_03() line 3 at SQL statement +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..61d46a792ec --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,168 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func00_01(); +SELECT svartest_plpgsql_func00_02(); +SELECT svartest_plpgsql_func00_03(); + +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); + +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.51.0 [text/x-patch] v20250913-0010-svariableReceiver.patch (8.9K, 8-v20250913-0010-svariableReceiver.patch) download | inline diff: From 75f46c6dacdb0fe8d7a54d3059919653c8a96848 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f32dcc29023..3ae88d9241d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2656,6 +2656,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250913-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250913-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 9653c91f89570cdce9a9f6d73606807b33096cb1 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index ff12e2e1364..ca4e77cd5b4 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +198,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 71857feae48..a86b9a53d1a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -647,6 +647,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -706,6 +716,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index f8d95c8e330..f32dcc29023 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2713,6 +2713,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.51.0 [text/x-patch] v20250913-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250913-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 5a435037e29b12a25fbd9e5241bf9d05fcf75d18 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 41bd8353430..401f71d0073 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -350,6 +350,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -588,6 +589,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -770,6 +774,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index d706546f332..9b8be530b83 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1320,6 +1323,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2020,8 +2067,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2111,6 +2159,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2120,6 +2175,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2138,6 +2197,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2199,7 +2292,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2221,7 +2317,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3614,6 +3711,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index ae0bd073ca9..e8fffb73291 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 5543440a33e..926a7b3a096 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -1991,9 +1991,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 4a903d1ec18..d33ad33798d 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +535,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 29d7732d6a0..35971e3a338 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.51.0 [text/x-patch] v20250913-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250913-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From b3bae1b02f016eb835a6e82f53e92fe312977e92 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index c37fd5da50b..97cd13957bb 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1717,8 +1717,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d6585c2e6e5..f8d95c8e330 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2654,6 +2654,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250913-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250913-0006-session-variable-fences-parsing.patch) download | inline diff: From 44855a5e2af9d82c4346f9c8741683639e552a93 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 78cabf964ef..1d053a35ca0 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3496,6 +3496,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index b9763ea1714..fd10c133afc 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2446,6 +2451,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2513,6 +2519,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2989,6 +2996,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6926cea75b9..a9d69f2a1e9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15691,6 +15691,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17066,6 +17079,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e1979a80c19..d17d0583cd4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -76,6 +77,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -105,7 +107,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -369,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -902,6 +910,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3121,6 +3258,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 905c975d83b..8b5240cd54b 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2033,6 +2033,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 1c4031eea23..b9b0ac55475 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3946,3 +3946,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 5daa308f4eb..49d116fa60a 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2b21c8f8c3f..d6585c2e6e5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3201,6 +3201,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.51.0 [text/x-patch] v20250913-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250913-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 17f5d50af095ad0444b779fc7560a31aeac47cad Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 63 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 296 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 05b84c0d6e7..15cbe5616fa 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -552,6 +552,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index d9041dad720..dac067bdc4c 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 058b5d659ba..0c9391c3e0a 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3207,6 +3207,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3780,6 +3788,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b4c45ad803e..affc467f1cc 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5681,6 +5682,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11813,6 +11996,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16319,6 +16505,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20132,6 +20321,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index bcc94ff07cc..31e2b67deee 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -745,6 +746,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -829,5 +843,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 2d02456664b..6b3fb9840ae 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1749,6 +1751,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index e7a2d64f741..84153db459d 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -962,6 +962,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1962,6 +1972,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4315,6 +4342,23 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4779,6 +4823,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4694653a91d..2b21c8f8c3f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3209,6 +3209,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.51.0 [text/x-patch] v20250913-0004-support-of-session-variables-for-psql.patch (22.8K, 14-v20250913-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 4aed402a8f8813c792421aa7624d8367a2c92492 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ea49a0bc82d..78cabf964ef 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5291,3 +5291,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 4aa793d7de7..1f5f7ced772 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6b20a4404b2..928b9dda714 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2621,6 +2629,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3227,7 +3238,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4041,6 +4052,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4318,6 +4336,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4519,7 +4543,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4527,6 +4553,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4541,7 +4568,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4549,7 +4577,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4585,6 +4614,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4911,7 +4942,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5400,6 +5431,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f6dd9dadec5..593bd95eb23 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6709,6 +6709,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.51.0 [text/x-patch] v20250913-0003-GRANT-REVOKE-variable.patch (52.5K, 15-v20250913-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 8dfbeb9447f968c35a0524d15fc4ba51f5d1f851 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 ++++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 +++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 307 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 158 +++++++++ 18 files changed, 915 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 487a58eda74..af178013cf4 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..a57f7665054 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 00e3630e0ec..93c10beebe9 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -290,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -534,6 +538,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -639,6 +647,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -773,6 +784,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -859,6 +882,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1022,6 +1071,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1222,6 +1275,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1469,6 +1527,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1529,6 +1590,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2762,6 +2826,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2784,7 +2851,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3025,6 +3091,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4288,6 +4356,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 3817efb1eeb..333be0b2ced 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2029,17 +2029,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3921,6 +3925,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5851,6 +5865,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 51b8778db5c..6926cea75b9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8059,6 +8059,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8104,6 +8112,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8302,6 +8318,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18122,6 +18139,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18779,6 +18797,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 7dadaefdfc1..e4522b72830 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "common/hashfn.h" @@ -128,6 +129,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -867,6 +870,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -964,6 +971,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5032,6 +5042,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 03e82d28c87..f6dd9dadec5 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5479,6 +5479,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..478b47472f0 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,307 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..e1a9d5ce5f4 --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,158 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.51.0 [text/x-patch] v20250913-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 16-v20250913-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From e020fdcac98f3fd3d486e70ea4a52ad82e02c6c3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index e9095bedf21..487a58eda74 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9810,4 +9815,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a13e8162890..3fe07f2ab64 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.51.0 [text/x-patch] v20250913-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 17-v20250913-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From d33327ace0af7e660316b6c2e045bef20cd75dec Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8651f0cdb91..c37fd5da50b 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1711,6 +1711,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index cd139bd65a6..00e3630e0ec 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2784,6 +2784,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2891,6 +2892,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 8bd4d6c3d43..ea49a0bc82d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "common/hashfn_unstable.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -224,6 +225,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -985,6 +987,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3288,6 +3368,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 91f3018fd0a..3817efb1eeb 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -635,6 +636,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -830,6 +845,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -855,6 +873,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1126,6 +1145,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2101,6 +2123,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2295,6 +2335,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2466,6 +2507,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3495,6 +3537,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4669,6 +4737,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6019,6 +6091,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 16e3e5c7457..6e3e8813328 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index cb75e11fced..aaf61a6f61c 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index f34868da5ab..6b46aeaef59 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2281,6 +2281,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2364,6 +2366,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3be2e051d32..79b922d0742 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6913,6 +6914,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6976,6 +6978,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index c6de04819f1..4749f3f7e56 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3391,6 +3392,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9fd48acb1f8..51b8778db5c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5308,6 +5311,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7119,6 +7150,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10054,6 +10086,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10415,6 +10465,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10696,6 +10764,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18045,6 +18121,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18701,6 +18778,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index e96b38a59d5..059d8f7f180 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4112,6 +4113,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4180,6 +4182,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4193,6 +4204,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 5f442bc3bd4..25413050601 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2334,6 +2340,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2642,6 +2651,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3218,6 +3230,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3752,6 +3768,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fa7cd7e06a7..1c4031eea23 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -40,6 +40,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3881,3 +3882,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index c65cee4f24c..5daa308f4eb 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3fe07f2ab64..4694653a91d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.51.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-09-15 08:21 Jim Jones <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Jim Jones @ 2025-09-15 08:21 UTC (permalink / raw) To: Pavel Stehule <[email protected]>; Bruce Momjian <[email protected]>; +Cc: Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi On 9/13/25 11:28, Pavel Stehule wrote: > > minor change (after private talk with Jim Jones) > After another pass, there are a few additional tests that should be included in this patch: == Additional tests for GRANT x ON VARIABLE == ============================================== We wanna make sure the proper error message is raised. postgres=# GRANT INSERT ON VARIABLE var TO jim; ERROR: invalid privilege type INSERT for session variable postgres=# GRANT DELETE ON VARIABLE var TO jim; ERROR: invalid privilege type DELETE for session variable == Tests for ALTER DEFAULT PRIVILEGES == ======================================== I couldn't find regression tests for ALTER DEFAULT PRIVILEGES ... if I haven't just missed them, I think they'd be a nice addition. postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT UPDATE ON VARIABLES TO jim; ALTER DEFAULT PRIVILEGES postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT INSERT ON VARIABLES TO jim; ERROR: invalid privilege type INSERT for session variable postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT DELETE ON VARIABLES TO jim; ERROR: invalid privilege type DELETE for session variable postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT SELECT ON VARIABLES TO jim; ALTER DEFAULT PRIVILEGES Best, Jim ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-09-15 16:55 Pavel Stehule <[email protected]> parent: Jim Jones <[email protected]> 0 siblings, 1 reply; 24+ messages in thread From: Pavel Stehule @ 2025-09-15 16:55 UTC (permalink / raw) To: Jim Jones <[email protected]>; +Cc: Bruce Momjian <[email protected]>; Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi po 15. 9. 2025 v 10:21 odesílatel Jim Jones <[email protected]> napsal: > Hi > > On 9/13/25 11:28, Pavel Stehule wrote: > > > > minor change (after private talk with Jim Jones) > > > > After another pass, there are a few additional tests that should be > included in this patch: > > > == Additional tests for GRANT x ON VARIABLE == > ============================================== > > We wanna make sure the proper error message is raised. > > postgres=# GRANT INSERT ON VARIABLE var TO jim; > ERROR: invalid privilege type INSERT for session variable > > postgres=# GRANT DELETE ON VARIABLE var TO jim; > ERROR: invalid privilege type DELETE for session variable > > > == Tests for ALTER DEFAULT PRIVILEGES == > ======================================== > > I couldn't find regression tests for ALTER DEFAULT PRIVILEGES ... if I > haven't just missed them, I think they'd be a nice addition. > > postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT UPDATE ON > VARIABLES TO jim; > ALTER DEFAULT PRIVILEGES > > postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT INSERT ON > VARIABLES TO jim; > ERROR: invalid privilege type INSERT for session variable > > postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT DELETE ON > VARIABLES TO jim; > ERROR: invalid privilege type DELETE for session variable > > postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA s GRANT SELECT ON > VARIABLES TO jim; > ALTER DEFAULT PRIVILEGES > done Regards Pavel > > > Best, Jim > Attachments: [text/x-patch] v20250915-0012-function-pg_get_session_variables_memory-for-cleanin.patch (4.9K, 3-v20250915-0012-function-pg_get_session_variables_memory-for-cleanin.patch) download | inline diff: From d22e3a168d3d9b5e64e6934ae04a39f8ed6a8ecd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_get_session_variables_memory for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..73ebf0340e3 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_get_session_variables_memory - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_get_session_variables_memory(PG_FUNCTION_ARGS) +{ +#define NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + bool nulls[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 593bd95eb23..cc086f3faf0 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12611,4 +12611,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'internal view of memory entries used by session variables (for debugging)', + proname => 'pg_get_session_variables_memory', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,dropped,can_select,can_update}', + prosrc => 'pg_get_session_variables_memory' }, ] -- 2.51.0 [text/x-patch] v20250915-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.4K, 4-v20250915-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From 90c0c24d0c2ba9ecc04071f01badf0398518cc6d Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 52126a10f62..cb7aa1a11ed 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..b69dac2df7a --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 5afae33d370..3068ae401db 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -120,3 +120,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..72629321d90 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, dropped FROM pg_get_session_variables_memory(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..3aef7665edc 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..a616ecdc8ef 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.51.0 [text/x-patch] v20250915-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 5-v20250915-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From ccdc194c38e66d88f96b34bebf097ed977fda6ae Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index ca4e77cd5b4..2b32f1cbda4 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 401f71d0073..ea16431e22b 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -352,6 +352,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -592,6 +606,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 9b8be530b83..8ee6e38869e 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2228,6 +2228,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3713,7 +3734,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3815,9 +3836,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3840,6 +3862,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index fd10c133afc..19c1936e302 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -83,6 +84,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -330,6 +333,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -409,6 +413,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -460,6 +469,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3359,6 +3369,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a9d69f2a1e9..441b6d03af5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12916,6 +12917,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17991,6 +18024,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18605,6 +18639,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 1a0e11ba701..ab0400c1f7a 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 928b9dda714..41b6a2c8d86 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1252,8 +1252,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4781,6 +4781,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 10cf52467e3..76fa4a29df2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ed5127942e0..19f25578ba1 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 35971e3a338..9d484315f70 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a9bd5970d85..ada28a3ac18 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.51.0 [text/x-patch] v20250915-0015-plpgsql-tests.patch (13.6K, 6-v20250915-0015-plpgsql-tests.patch) download | inline diff: From 747216022d10a5e0ef60f89172d2af01b7be0654 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 239 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 168 ++++++++++++ 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ec5ffcb42b2 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,239 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func00_01(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: VARIABLE(this_variable_doesnt_exists) + ^ +QUERY: VARIABLE(this_variable_doesnt_exists) +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_01() line 3 at RAISE +SELECT svartest_plpgsql_func00_02(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists = 10 + ^ +QUERY: LET this_variable_doesnt_exists = 10 +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_02() line 3 at SQL statement +SELECT svartest_plpgsql_func00_03(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists[10] = 'Hello' + ^ +QUERY: LET this_variable_doesnt_exists[10] = 'Hello' +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_03() line 3 at SQL statement +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..61d46a792ec --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,168 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func00_01(); +SELECT svartest_plpgsql_func00_02(); +SELECT svartest_plpgsql_func00_03(); + +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); + +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.51.0 [text/x-patch] v20250915-0013-DISCARD-VARIABLES.patch (10.2K, 7-v20250915-0013-DISCARD-VARIABLES.patch) download | inline diff: From df4c869f0f610ae33f8b04024cd6bc18c59401f2 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 73ebf0340e3..52126a10f62 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 441b6d03af5..f606f392070 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index ab0400c1f7a..7a9271d0639 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 41b6a2c8d86..dca77b9dab6 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4208,7 +4208,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 76fa4a29df2..c9aa0654104 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..dfbe7900c1c 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..b0aeeb865c2 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.51.0 [text/x-patch] v20250915-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 8-v20250915-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 91b11b538711f500d91e2db69a76b09439f9179e Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index ff12e2e1364..ca4e77cd5b4 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +198,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 3a920cc7d17..86f5dc6a8e3 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -645,6 +645,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -704,6 +714,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a4435b620b1..52d2b6d56d4 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2713,6 +2713,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.51.0 [text/x-patch] v20250915-0010-svariableReceiver.patch (8.9K, 9-v20250915-0010-svariableReceiver.patch) download | inline diff: From 248d73ab0806764ca329ed93b96c4439a28ffebf Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 52d2b6d56d4..a9bd5970d85 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2656,6 +2656,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250915-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250915-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 21687a0eae20afbd22a74eab24e96929f24ba0dd Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 41bd8353430..401f71d0073 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -350,6 +350,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -588,6 +589,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -770,6 +774,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index d706546f332..9b8be530b83 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1320,6 +1323,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2020,8 +2067,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2111,6 +2159,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2120,6 +2175,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2138,6 +2197,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2199,7 +2292,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2221,7 +2317,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3614,6 +3711,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index ae0bd073ca9..e8fffb73291 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2395,6 +2403,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2521,6 +2530,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4819,7 +4849,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index b4c1e2c4b21..de1ffeed317 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -1990,9 +1990,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index b12a2508d8c..ed5127942e0 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -526,6 +529,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 29d7732d6a0..35971e3a338 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.51.0 [text/x-patch] v20250915-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250915-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 5ee34e9adca5d4411b720315dc6eecd46bd1bcf8 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index c37fd5da50b..97cd13957bb 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1717,8 +1717,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d73b44f7cf7..a4435b620b1 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2654,6 +2654,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250915-0006-session-variable-fences-parsing.patch (32.4K, 12-v20250915-0006-session-variable-fences-parsing.patch) download | inline diff: From c0657ca7662a201f63a10c7b5ab36ad6d7be091c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 9 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 661 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 78cabf964ef..1d053a35ca0 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3496,6 +3496,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index b9763ea1714..fd10c133afc 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -607,6 +607,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1032,6 +1033,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1497,6 +1499,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1723,6 +1726,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1974,6 +1978,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2446,6 +2451,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2513,6 +2519,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2989,6 +2996,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6926cea75b9..a9d69f2a1e9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15691,6 +15691,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17066,6 +17079,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e1979a80c19..d17d0583cd4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -76,6 +77,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -105,7 +107,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -369,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -902,6 +910,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3121,6 +3258,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 905c975d83b..8b5240cd54b 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2033,6 +2033,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..52b8673eed9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8803,6 +8805,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13566,6 +13576,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 1c4031eea23..b9b0ac55475 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3946,3 +3946,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..b221adafd2f 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -98,6 +98,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 2b4c7363d74..10cf52467e3 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 03c4c58580e..5926a854d12 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index de2ffcdab69..d73b44f7cf7 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3201,6 +3201,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.51.0 [text/x-patch] v20250915-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250915-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 32291f12d2b65d2f73b8e0ac2d1116b8fd9633d0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 63 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 296 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 05b84c0d6e7..15cbe5616fa 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -552,6 +552,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index d9041dad720..dac067bdc4c 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 058b5d659ba..0c9391c3e0a 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3207,6 +3207,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3780,6 +3788,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index b4c45ad803e..affc467f1cc 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5681,6 +5682,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11813,6 +11996,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16319,6 +16505,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20132,6 +20321,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index bcc94ff07cc..31e2b67deee 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -745,6 +746,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -829,5 +843,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 2d02456664b..6b3fb9840ae 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1749,6 +1751,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index e7a2d64f741..84153db459d 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -962,6 +962,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -1962,6 +1972,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4315,6 +4342,23 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4779,6 +4823,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 59eb79826fb..de2ffcdab69 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3209,6 +3209,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.51.0 [text/x-patch] v20250915-0004-support-of-session-variables-for-psql.patch (22.8K, 14-v20250915-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 198a963c22fcea512f46058c3c6335769431655b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ea49a0bc82d..78cabf964ef 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5291,3 +5291,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 4aa793d7de7..1f5f7ced772 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6b20a4404b2..928b9dda714 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2621,6 +2629,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3227,7 +3238,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4041,6 +4052,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4318,6 +4336,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4519,7 +4543,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4527,6 +4553,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4541,7 +4568,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4549,7 +4577,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4585,6 +4614,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4911,7 +4942,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5400,6 +5431,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f6dd9dadec5..593bd95eb23 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6709,6 +6709,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.51.0 [text/x-patch] v20250915-0003-GRANT-REVOKE-variable.patch (54.4K, 15-v20250915-0003-GRANT-REVOKE-variable.patch) download | inline diff: From df0753f3e888b7f0d147b7eeaf137f1778fc8e71 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 +++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 ++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 335 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 182 ++++++++++ 18 files changed, 967 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 487a58eda74..af178013cf4 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..a57f7665054 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 00e3630e0ec..93c10beebe9 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -290,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -534,6 +538,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -639,6 +647,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -773,6 +784,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -859,6 +882,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1022,6 +1071,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1222,6 +1275,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1469,6 +1527,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1529,6 +1590,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2762,6 +2826,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2784,7 +2851,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3025,6 +3091,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4288,6 +4356,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 3817efb1eeb..333be0b2ced 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2029,17 +2029,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3921,6 +3925,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5851,6 +5865,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 51b8778db5c..6926cea75b9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8059,6 +8059,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8104,6 +8112,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8302,6 +8318,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18122,6 +18139,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18779,6 +18797,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 7dadaefdfc1..e4522b72830 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "common/hashfn.h" @@ -128,6 +129,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -867,6 +870,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -964,6 +971,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5032,6 +5042,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 03e82d28c87..f6dd9dadec5 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5479,6 +5479,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..f2219529916 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,335 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT UPDATE ON VARIABLES TO regress_variable_reader_acl; +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT INSERT ON VARIABLES TO regress_variable_reader_acl; +ERROR: invalid privilege type INSERT for session variable +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT DELETE ON VARIABLES TO regress_variable_reader_acl; +ERROR: invalid privilege type DELETE for session variable +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +-- should to fail +GRANT INSERT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +ERROR: invalid privilege type INSERT for session variable +GRANT DELETE ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +ERROR: invalid privilege type DELETE for session variable +-- should be ok +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..4b3653cd4ed --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,182 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT UPDATE ON VARIABLES TO regress_variable_reader_acl; + +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT INSERT ON VARIABLES TO regress_variable_reader_acl; + +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT DELETE ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'UPDATE'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; + +-- should to fail +GRANT INSERT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +GRANT DELETE ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +-- should be ok +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.51.0 [text/x-patch] v20250915-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250915-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 58f106fda9b5768d81c04442129b9a01bb9edde3 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8651f0cdb91..c37fd5da50b 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1711,6 +1711,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index cd139bd65a6..00e3630e0ec 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2784,6 +2784,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2891,6 +2892,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 8bd4d6c3d43..ea49a0bc82d 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "common/hashfn_unstable.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -224,6 +225,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -985,6 +987,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3288,6 +3368,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 91f3018fd0a..3817efb1eeb 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -635,6 +636,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -830,6 +845,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -855,6 +873,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1126,6 +1145,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2101,6 +2123,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2295,6 +2335,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2466,6 +2507,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3495,6 +3537,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4669,6 +4737,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6019,6 +6091,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 16e3e5c7457..6e3e8813328 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index cb75e11fced..aaf61a6f61c 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index f34868da5ab..6b46aeaef59 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2281,6 +2281,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2364,6 +2366,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3be2e051d32..79b922d0742 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6913,6 +6914,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6976,6 +6978,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index c6de04819f1..4749f3f7e56 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3391,6 +3392,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9fd48acb1f8..51b8778db5c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5308,6 +5311,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7119,6 +7150,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10054,6 +10086,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10415,6 +10465,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10696,6 +10764,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18045,6 +18121,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18701,6 +18778,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index e96b38a59d5..059d8f7f180 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4112,6 +4113,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4180,6 +4182,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4193,6 +4204,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 918db53dd5e..1a0e11ba701 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fa7cd7e06a7..1c4031eea23 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -40,6 +40,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3881,3 +3882,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c..bdac0c13bec 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -97,6 +97,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -169,6 +171,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 86a236bd58b..2b4c7363d74 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 50fb149e9ac..03c4c58580e 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 777ca86b575..59eb79826fb 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.51.0 [text/x-patch] v20250915-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250915-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From 011d594bfc710acb94543d5bf390e7fb771b1d64 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index e9095bedf21..487a58eda74 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9810,4 +9815,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e90af5b2ad3..777ca86b575 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.51.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: proposal: schema variables @ 2025-09-29 10:13 Pavel Stehule <[email protected]> parent: Pavel Stehule <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Pavel Stehule @ 2025-09-29 10:13 UTC (permalink / raw) To: Jim Jones <[email protected]>; +Cc: Bruce Momjian <[email protected]>; Dmitry Dolgov <[email protected]>; Laurenz Albe <[email protected]>; Erik Rijkers <[email protected]>; Michael Paquier <[email protected]>; Amit Kapila <[email protected]>; DUVAL REMI <[email protected]>; PostgreSQL Hackers <[email protected]>; jian he <[email protected]>; Alvaro Herrera <[email protected]>; PegoraroF10 <[email protected]> Hi fresh rebase Regards Pavel Attachments: [text/x-patch] v20250929-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.4K, 3-v20250929-0014-memory-cleaning-after-DROP-VARIABLE.patch) download | inline diff: From d96afa3b277f677ca6623421cefdba7e4c0d1573 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 22:33:25 +0200 Subject: [PATCH 14/15] memory cleaning after DROP VARIABLE Accepting a sinval message invalidates entries in the "sessionvars" hash table. These entries are validated before any read or write operations on session variables. When the entry cannot be validated, it is removed. Removal will be delayed when the variable was dropped by the current transaction, which could still be rolled back. --- src/backend/catalog/pg_variable.c | 7 +- src/backend/commands/session_variable.c | 154 ++++++++++++- src/include/commands/session_variable.h | 2 + .../isolation/expected/session-variable.out | 110 +++++++++ src/test/isolation/isolation_schedule | 1 + .../isolation/specs/session-variable.spec | 50 ++++ .../expected/session_variables_ddl.out | 214 ++++++++++++++++++ .../regress/sql/session_variables_ddl.sql | 151 ++++++++++++ 8 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/test/isolation/expected/session-variable.out create mode 100644 src/test/isolation/specs/session-variable.spec diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index d8ede4fa8c8..c9411443c6d 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -22,6 +22,7 @@ #include "catalog/pg_collation.h" #include "catalog/pg_namespace.h" #include "catalog/pg_variable.h" +#include "commands/session_variable.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" #include "utils/syscache.h" @@ -154,7 +155,8 @@ create_variable(const char *varName, } /* - * Drop variable by OID + * Drop variable by OID, and register the needed session variable + * cleanup. */ void DropVariableById(Oid varid) @@ -174,4 +176,7 @@ DropVariableById(Oid varid) ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); + + /* do the necessary cleanup in local memory, if needed */ + SessionVariableDropPostprocess(varid); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 52126a10f62..cb7aa1a11ed 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -13,8 +13,8 @@ *------------------------------------------------------------------------- */ #include "postgres.h" - #include "access/htup_details.h" +#include "access/xact.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" @@ -76,6 +76,14 @@ typedef struct SVariableData void *domain_check_extra; LocalTransactionId domain_check_extra_lxid; + /* + * Top level local transaction id of the last transaction that dropped the + * variable, if any. We need this information to avoid freeing memory for + * variables dropped by the local backend, in case the operation is rolled + * back. + */ + LocalTransactionId drop_lxid; + /* * Stored value and type description can be outdated when we receive a * sinval message. We then have to check if the stored data are still @@ -92,6 +100,17 @@ static HTAB *sessionvars = NULL; /* hash table for session variables */ static MemoryContext SVariableMemoryContext = NULL; +/* becomes true when we receive a sinval message */ +static bool needs_validation = false; + +/* + * The content of dropped session variables is not removed immediately. We do + * that in the next transaction that reads or writes a session variable. + * "validated_lxid" stores the transaction that performed said validation, so + * that we can avoid repeating the effort. + */ +static LocalTransactionId validated_lxid = InvalidLocalTransactionId; + /* * Callback function for session variable invalidation. */ @@ -124,6 +143,38 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) if (hashvalue == 0 || svar->hashvalue == hashvalue) { svar->is_valid = false; + needs_validation = true; + } + } +} + +/* + * Handle the local memory cleanup for a DROP VARIABLE command. + * + * Caller should take care of removing the pg_variable entry first. + */ +void +SessionVariableDropPostprocess(Oid varid) +{ + Assert(LocalTransactionIdIsValid(MyProc->vxid.lxid)); + + if (sessionvars) + { + bool found; + SVariable svar = (SVariable) hash_search(sessionvars, &varid, + HASH_FIND, &found); + + if (found) + { + /* + * Save the current top level local transaction id to make sure we + * won't automatically remove the local variable storage in + * validate_all_session_variables() when the invalidation message + * from DROP VARIABLE arrives. After all, the transaction could + * still be rolled back. + */ + svar->is_valid = false; + svar->drop_lxid = MyProc->vxid.lxid; } } } @@ -177,6 +228,67 @@ is_session_variable_valid(SVariable svar) return result; } +/* + * Check all potentially invalid session variable data in local memory and free + * the memory for all invalid ones. This function is called before any read or + * write of a session variable. Freeing of a variable's memory is postponed if + * the variable has been dropped by the current transaction, since that + * operation could still be rolled back. + * + * It is possible that we receive a cache invalidation message while + * remove_invalid_session_variables() is executing, so we cannot guarantee that + * all entries in "sessionvars" will be set to "is_valid" after the function is + * done. However, we can guarantee that all entries get checked once. + */ +static void +remove_invalid_session_variables(void) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + /* + * The validation requires system catalog access, so the session state + * should be "in transaction". + */ + Assert(IsTransactionState()); + + if (!needs_validation || !sessionvars) + return; + + /* + * Reset the flag before we start the validation. It can be set again by + * concurrently incoming sinval messages. + */ + needs_validation = false; + + elog(DEBUG1, "effective call of validate_all_session_variables()"); + + hash_seq_init(&status, sessionvars); + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (!svar->is_valid) + { + if (svar->drop_lxid == MyProc->vxid.lxid) + { + /* try again in the next transaction */ + needs_validation = true; + continue; + } + + if (!is_session_variable_valid(svar)) + { + Oid varid = svar->varid; + + free_session_variable_value(svar); + hash_search(sessionvars, &varid, HASH_REMOVE, NULL); + svar = NULL; + } + else + svar->is_valid = true; + } + } +} + /* * Initialize attributes cached in "svar" */ @@ -206,6 +318,8 @@ setup_session_variable(SVariable svar, Oid varid) svar->domain_check_extra = NULL; svar->domain_check_extra_lxid = InvalidLocalTransactionId; + svar->drop_lxid = InvalidTransactionId; + svar->isnull = true; svar->value = (Datum) 0; @@ -329,22 +443,42 @@ get_session_variable(Oid varid) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); if (found) { + /* + * The session variable could have been dropped by a DROP VARIABLE + * statement in a subtransaction that was later rolled back, which + * means that we may have to work with the data of a variable marked + * as invalid. + */ if (!svar->is_valid) { /* - * If there was an invalidation message, the variable might still - * be valid, but we have to check with the system catalog. + * We have to check the system catalog to see if the variable is + * still valid, even if an invalidation message set it to invalid. + * + * The variable must be validated before it is accessed. The oid + * should be valid, because the related session variable is + * already locked, and remove_invalid_session_variables() would + * remove variables dropped by other transactions. */ if (is_session_variable_valid(svar)) svar->is_valid = true; else - /* if the value cannot be validated, we have to discard it */ - free_session_variable_value(svar); + elog(ERROR, "unexpected state of session variable %u", varid); } } else @@ -405,6 +539,16 @@ SetSessionVariable(Oid varid, Datum value, bool isNull) if (!sessionvars) create_sessionvars_hashtables(); + if (validated_lxid == InvalidLocalTransactionId || + validated_lxid != MyProc->vxid.lxid) + { + /* free the memory from dropped session variables */ + remove_invalid_session_variables(); + + /* don't repeat the above step in the same transaction */ + validated_lxid = MyProc->vxid.lxid; + } + svar = (SVariable) hash_search(sessionvars, &varid, HASH_ENTER, &found); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index ac36dfcc19b..c06e1faf02c 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -21,6 +21,8 @@ #include "nodes/parsenodes.h" #include "tcop/cmdtag.h" +extern void SessionVariableDropPostprocess(Oid varid); + extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); diff --git a/src/test/isolation/expected/session-variable.out b/src/test/isolation/expected/session-variable.out new file mode 100644 index 00000000000..b69dac2df7a --- /dev/null +++ b/src/test/isolation/expected/session-variable.out @@ -0,0 +1,110 @@ +Parsed test spec with 4 sessions + +starting permutation: let val drop val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist + +starting permutation: let val s1 drop val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step drop: DROP VARIABLE myvar; +step val: SELECT VARIABLE(myvar); +ERROR: session variable "myvar" doesn't exist +step sr1: ROLLBACK; + +starting permutation: let val dbg drop create dbg val +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + + +starting permutation: let val s1 dbg drop create dbg val sr1 +step let: LET myvar = 'test'; +step val: SELECT VARIABLE(myvar); +myvar +----- +test +(1 row) + +step s1: BEGIN; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name |dropped +------+-----+------- +public|myvar|f +(1 row) + +step drop: DROP VARIABLE myvar; +step create: CREATE VARIABLE myvar AS text; +step dbg: SELECT schema, name, dropped FROM pg_get_session_variables_memory(); +schema|name|dropped +------+----+------- + | |t +(1 row) + +step val: SELECT VARIABLE(myvar); +myvar +----- + +(1 row) + +step sr1: ROLLBACK; + +starting permutation: create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state +step create3: CREATE VARIABLE myvar3 AS text; +step let3: LET myvar3 = 'test'; +step s3: BEGIN; +step create4: CREATE VARIABLE myvar4 AS text; +step let4: LET myvar4 = 'test'; +step drop4: DROP VARIABLE myvar4; +step drop3: DROP VARIABLE myvar3; +step inval3: SELECT COUNT(*) >= 0 FROM pg_foreign_table; +?column? +-------- +t +(1 row) + +step discard: DISCARD VARIABLES; +step sc3: COMMIT; +step state: SELECT varname FROM pg_variable; +varname +------- +myvar +(1 row) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 5afae33d370..3068ae401db 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -120,3 +120,4 @@ test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: session-variable diff --git a/src/test/isolation/specs/session-variable.spec b/src/test/isolation/specs/session-variable.spec new file mode 100644 index 00000000000..72629321d90 --- /dev/null +++ b/src/test/isolation/specs/session-variable.spec @@ -0,0 +1,50 @@ +# Test session variables memory cleanup for sinval + +setup +{ + CREATE VARIABLE myvar AS text; +} + +teardown +{ + DROP VARIABLE IF EXISTS myvar; +} + +session s1 +step s1 { BEGIN; } +step let { LET myvar = 'test'; } +step val { SELECT VARIABLE(myvar); } +step dbg { SELECT schema, name, dropped FROM pg_get_session_variables_memory(); } +step sr1 { ROLLBACK; } + +session s2 +step drop { DROP VARIABLE myvar; } +step create { CREATE VARIABLE myvar AS text; } + +session s3 +step s3 { BEGIN; } +step let3 { LET myvar3 = 'test'; } +step create4 { CREATE VARIABLE myvar4 AS text; } +step let4 { LET myvar4 = 'test'; } +step drop4 { DROP VARIABLE myvar4; } +step inval3 { SELECT COUNT(*) >= 0 FROM pg_foreign_table; } +step discard { DISCARD VARIABLES; } +step sc3 { COMMIT; } +step state { SELECT varname FROM pg_variable; } + +session s4 +step create3 { CREATE VARIABLE myvar3 AS text; } +step drop3 { DROP VARIABLE myvar3; } + +# Concurrent drop of a known variable should lead to an error +permutation let val drop val +# Same, but with an explicit transaction +permutation let val s1 drop val sr1 +# Concurrent drop/create of a known variable should lead to empty variable +permutation let val dbg drop create dbg val +# Concurrent drop/create of a known variable should lead to empty variable +# We need a transaction to make sure that we won't accept invalidation when +# calling the dbg step after the concurrent drop +permutation let val s1 dbg drop create dbg val sr1 +# test for DISCARD ALL when all internal queues have actions registered +permutation create3 let3 s3 create4 let4 drop4 drop3 inval3 discard sc3 state diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out index 9c7595e9a41..3aef7665edc 100644 --- a/src/test/regress/expected/session_variables_ddl.out +++ b/src/test/regress/expected/session_variables_ddl.out @@ -161,3 +161,217 @@ DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 drop cascades to session variable svartest01_ddl.sesvar11 DROP SCHEMA svartest02_ddl CASCADE; NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 +CREATE SCHEMA svartest_ddl; +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction +LET svartest_ddl.sesvar60 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; +LET svartest_ddl.sesvar60 = 'Hello'; +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); +ERROR: session variable "svartest_ddl.sesvar60" doesn't exist +LINE 1: SELECT VARIABLE(svartest_ddl.sesvar60); + ^ +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + sesvar60 +---------- + 100 +(1 row) + +ROLLBACK; +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + sesvar60 +---------- + Hello +(1 row) + +DROP VARIABLE svartest_ddl.sesvar60; +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); + sesvar60 +---------- + 100 +(1 row) + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + count +------- + 1 +(1 row) + + DROP VARIABLE svartest_ddl.sesvar60; +COMMIT; +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + count +------- + 0 +(1 row) + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s2; +ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + SAVEPOINT s2; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + + ROLLBACK TO s1; + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + sesvar62 +---------- + 0 +(1 row) + +COMMIT; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + sesvar61 +---------- + 10 +(1 row) + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; +DROP SCHEMA svartest_ddl; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql index f844469ecb1..a616ecdc8ef 100644 --- a/src/test/regress/sql/session_variables_ddl.sql +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -148,3 +148,154 @@ ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; DROP SCHEMA svartest01_ddl CASCADE; DROP SCHEMA svartest02_ddl CASCADE; + +CREATE SCHEMA svartest_ddl; + +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +-- dropped variables should be removed from memory before the next usage +-- of any session variable in the next transaction + +LET svartest_ddl.sesvar60 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +-- the content of the value should be preserved when a variable is dropped +-- by an aborted transaction +CREATE VARIABLE svartest_ddl.sesvar60 AS varchar; + +LET svartest_ddl.sesvar60 = 'Hello'; + +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should fail + SELECT VARIABLE(svartest_ddl.sesvar60); + +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +-- another test +BEGIN; + DROP VARIABLE svartest_ddl.sesvar60; + + -- should be ok + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + LET svartest_ddl.sesvar60 = 100; + SELECT VARIABLE(svartest_ddl.sesvar60); -- 100 + +ROLLBACK; + +SELECT VARIABLE(svartest_ddl.sesvar60); -- Hello + +DROP VARIABLE svartest_ddl.sesvar60; + +-- should be zero +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +BEGIN; + CREATE VARIABLE svartest_ddl.sesvar60 AS int; + + LET svartest_ddl.sesvar60 = 100; + + SELECT VARIABLE(svartest_ddl.sesvar60); + + SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 1 + + DROP VARIABLE svartest_ddl.sesvar60; + +COMMIT; + +SELECT count(*) FROM pg_get_session_variables_memory() + WHERE schema = 'svartest_ddl'; -- 0 + +CREATE VARIABLE svartest_ddl.sesvar61 AS int; +CREATE VARIABLE svartest_ddl.sesvar62 AS int; + +LET svartest_ddl.sesvar61 = 10; +LET svartest_ddl.sesvar62 = 0; + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + SELECT VARIABLE(svartest_ddl.sesvar61); + + SAVEPOINT s2; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s2; +ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +BEGIN; + SAVEPOINT s1; + DROP VARIABLE svartest_ddl.sesvar61; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + + SAVEPOINT s2; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); + ROLLBACK TO s1; + + -- force cleaning by touching another session variable + SELECT VARIABLE(svartest_ddl.sesvar62); +COMMIT; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +-- repeated aborted transaction +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; +BEGIN; DROP VARIABLE svartest_ddl.sesvar61; ROLLBACK; + +-- should be ok +SELECT VARIABLE(svartest_ddl.sesvar61); + +DROP VARIABLE svartest_ddl.sesvar61, svartest_ddl.sesvar62; + +DROP SCHEMA svartest_ddl; -- 2.51.0 [text/x-patch] v20250929-0012-function-pg_get_session_variables_memory-for-cleanin.patch (4.9K, 4-v20250929-0012-function-pg_get_session_variables_memory-for-cleanin.patch) download | inline diff: From 67875c23f617cb70b27fbb7e9d459f936c8b1839 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 19 Jan 2024 20:01:56 +0100 Subject: [PATCH 12/15] function pg_get_session_variables_memory for cleaning tests This is a function designed for testing and debugging. It returns the content of sessionvars as-is, and can therefore display entries about session variables that were dropped but for which this backend didn't process the shared invalidations yet. --- src/backend/commands/session_variable.c | 100 ++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 8 ++ 2 files changed, 108 insertions(+) diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 768163e2009..73ebf0340e3 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -22,6 +22,7 @@ #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/svariableReceiver.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/plannodes.h" #include "parser/parse_type.h" @@ -600,3 +601,102 @@ ExecuteLetStmt(ParseState *pstate, PopActiveSnapshot(); } + +/* + * pg_get_session_variables_memory - designed for testing + * + * This is a function designed for testing and debugging. It returns the + * content of session variables as-is, and can therefore display data about + * session variables that were dropped, but for which this backend didn't + * process the shared invalidations yet. + */ +Datum +pg_get_session_variables_memory(PG_FUNCTION_ARGS) +{ +#define NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS 8 + + /* + * Make sure syscache entries are flushed for recent catalog changes. For + * stable behavior we need to reliably detect which variables were + * dropped. + */ + AcceptInvalidationMessages(); + + InitMaterializedSRF(fcinfo, 0); + + if (sessionvars) + { + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + HASH_SEQ_STATUS status; + SVariable svar; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + Datum values[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + bool nulls[NUM_PG_GET_SESSION_VARIABLES_MEMORY_ATTS]; + HeapTuple tp; + bool var_is_valid = false; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = ObjectIdGetDatum(svar->varid); + values[3] = ObjectIdGetDatum(svar->typid); + + /* + * It is possible that the variable has been dropped from the + * catalog, but not yet purged from the hash table. + */ + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_variable varform = (Form_pg_variable) GETSTRUCT(tp); + + /* + * It is also possible that a variable has been dropped and + * someone created a new variable with the same object ID. + * Use the catalog information only if that is not the case. + */ + if (svar->create_lsn == varform->varcreate_lsn) + { + values[1] = CStringGetTextDatum( + get_namespace_name(varform->varnamespace)); + + values[2] = CStringGetTextDatum(NameStr(varform->varname)); + values[4] = CStringGetTextDatum(format_type_be(svar->typid)); + values[5] = BoolGetDatum(false); + + values[6] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_SELECT) == ACLCHECK_OK); + + values[7] = BoolGetDatum( + object_aclcheck(VariableRelationId, svar->varid, + GetUserId(), ACL_UPDATE) == ACLCHECK_OK); + + var_is_valid = true; + } + + ReleaseSysCache(tp); + } + + /* if there is no matching catalog entry, return null values */ + if (!var_is_valid) + { + nulls[1] = true; + nulls[2] = true; + nulls[4] = true; + values[5] = BoolGetDatum(true); + nulls[6] = true; + nulls[7] = true; + } + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + } + + return (Datum) 0; +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d28fb3283cb..597f56a4d95 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12611,4 +12611,12 @@ proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, +# Session variables support +{ oid => '8488', descr => 'internal view of memory entries used by session variables (for debugging)', + proname => 'pg_get_session_variables_memory', prorows => '1000', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{oid,text,text,oid,text,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o}', + proargnames => '{varid,schema,name,typid,typname,dropped,can_select,can_update}', + prosrc => 'pg_get_session_variables_memory' }, ] -- 2.51.0 [text/x-patch] v20250929-0015-plpgsql-tests.patch (13.6K, 5-v20250929-0015-plpgsql-tests.patch) download | inline diff: From f7ebf51a959e8e0ef999e9e7051d2bfd7a6deb63 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <[email protected]> Date: Wed, 13 Nov 2024 14:06:06 +0100 Subject: [PATCH 15/15] plpgsql tests set of plpgsql related tests: * check session variables and plpgsql variables are not in collision ever * check correct plpgsql plan cache invalidation when session variable is dropped * check so the value of session variable is not corrupted, when the variable is modified inside nested called functions --- src/pl/plpgsql/src/Makefile | 3 +- .../src/expected/plpgsql_session_variable.out | 239 ++++++++++++++++++ src/pl/plpgsql/src/meson.build | 1 + .../src/sql/plpgsql_session_variable.sql | 168 ++++++++++++ 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/pl/plpgsql/src/expected/plpgsql_session_variable.out create mode 100644 src/pl/plpgsql/src/sql/plpgsql_session_variable.sql diff --git a/src/pl/plpgsql/src/Makefile b/src/pl/plpgsql/src/Makefile index 63cb96fae3e..bbcae27d422 100644 --- a/src/pl/plpgsql/src/Makefile +++ b/src/pl/plpgsql/src/Makefile @@ -35,7 +35,8 @@ REGRESS_OPTS = --dbname=$(PL_TESTDB) 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 + plpgsql_trap plpgsql_trigger plpgsql_varprops \ + plpgsql_session_variable # where to find gen_keywordlist.pl and subsidiary files TOOLSDIR = $(top_srcdir)/src/tools diff --git a/src/pl/plpgsql/src/expected/plpgsql_session_variable.out b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out new file mode 100644 index 00000000000..ec5ffcb42b2 --- /dev/null +++ b/src/pl/plpgsql/src/expected/plpgsql_session_variable.out @@ -0,0 +1,239 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func00_01(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: VARIABLE(this_variable_doesnt_exists) + ^ +QUERY: VARIABLE(this_variable_doesnt_exists) +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_01() line 3 at RAISE +SELECT svartest_plpgsql_func00_02(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists = 10 + ^ +QUERY: LET this_variable_doesnt_exists = 10 +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_02() line 3 at SQL statement +SELECT svartest_plpgsql_func00_03(); +ERROR: session variable "this_variable_doesnt_exists" doesn't exist +LINE 1: LET this_variable_doesnt_exists[10] = 'Hello' + ^ +QUERY: LET this_variable_doesnt_exists[10] = 'Hello' +CONTEXT: PL/pgSQL function svartest_plpgsql_func00_03() line 3 at SQL statement +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func01_01(); +NOTICE: plpgsql var: 100, session var: 1000 + svartest_plpgsql_func01_01 +---------------------------- + +(1 row) + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +-- should fail +SELECT svartest_plpgsql_func01_02(); +ERROR: session variable "__plpgsql_sesvar01" doesn't exist +LINE 1: LET __plpgsql_sesvar01 = 1000 + ^ +QUERY: LET __plpgsql_sesvar01 = 1000 +CONTEXT: PL/pgSQL function svartest_plpgsql_func01_02() line 5 at SQL statement +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +ERROR: "plpgsql_sesvar01" is not a known variable +LINE 4: plpgsql_sesvar01 := 100; + ^ +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; +SELECT svartest_plpgsql_func02(); +NOTICE: {100} +NOTICE: {-1} + svartest_plpgsql_func02 +------------------------- + +(1 row) + +DROP FUNCTION svartest_plpgsql_func02(); +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; +LET plpgsql_sesvar02 = 0.0; +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 1 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 2 +(1 row) + +SELECT svartest_plpgsql_inc(1.0); + svartest_plpgsql_inc +---------------------- + 3 +(1 row) + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + svartest_plpgsql_inc +---------------------- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; +SELECT svartest_plpgsql_func03(); + svartest_plpgsql_func03 +------------------------- + abc +(1 row) + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); +DROP VARIABLE plpgsql_sesvar03; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d9..1d01d1c2629 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -88,6 +88,7 @@ tests += { 'plpgsql_trap', 'plpgsql_trigger', 'plpgsql_varprops', + 'plpgsql_session_variable', ], }, } diff --git a/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql new file mode 100644 index 00000000000..61d46a792ec --- /dev/null +++ b/src/pl/plpgsql/src/sql/plpgsql_session_variable.sql @@ -0,0 +1,168 @@ +-- variables are not checked in compile time +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_01() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(this_variable_doesnt_exists); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_02() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists = 10; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func00_03() +RETURNS void AS $$ +BEGIN + LET this_variable_doesnt_exists[10] = 'Hello'; +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func00_01(); +SELECT svartest_plpgsql_func00_02(); +SELECT svartest_plpgsql_func00_03(); + +DROP FUNCTION svartest_plpgsql_func00_01(); +DROP FUNCTION svartest_plpgsql_func00_02(); +DROP FUNCTION svartest_plpgsql_func00_03(); + +-- check of correct plan cache invalidation +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +-- plpgsql variables and session variables are not in collision ever +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_01() +RETURNS void AS $$ +DECLARE plpgsql_sesvar01 int; +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func01_01(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_02() +RETURNS void AS $$ +DECLARE __plpgsql_sesvar01 int; +BEGIN + __plpgsql_sesvar01 := 100; + LET __plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + __plpgsql_sesvar01, VARIABLE(__plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +-- should fail +SELECT svartest_plpgsql_func01_02(); + +-- should fail +CREATE OR REPLACE FUNCTION svartest_plpgsql_func01_03() +RETURNS void AS $$ +BEGIN + plpgsql_sesvar01 := 100; + LET plpgsql_sesvar01 = 1000; + RAISE NOTICE 'plpgsql var: %, session var: %', + plpgsql_sesvar01, VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION svartest_plpgsql_func01_01(); +DROP FUNCTION svartest_plpgsql_func01_02(); + +CREATE OR REPLACE FUNCTION svartest_plpgsql_func02() +RETURNS void AS $$ +DECLARE v int[] DEFAULT '{}'; +BEGIN + LET plpgsql_sesvar01 = 1; + v[VARIABLE(plpgsql_sesvar01)] = 100; + RAISE NOTICE '%', v; + LET plpgsql_sesvar02 = v; + LET plpgsql_sesvar02[VARIABLE(plpgsql_sesvar01)] = -1; + RAISE NOTICE '%', VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +CREATE VARIABLE plpgsql_sesvar01 AS int; +CREATE VARIABLE plpgsql_sesvar02 AS int[]; + +SELECT svartest_plpgsql_func02(); + +DROP FUNCTION svartest_plpgsql_func02(); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +-- returns updated value +CREATE VARIABLE plpgsql_sesvar01 AS int; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(int) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar01 = COALESCE(VARIABLE(plpgsql_sesvar01) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar01); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); +SELECT svartest_plpgsql_inc(1); + +SELECT svartest_plpgsql_inc(1) FROM generate_series(1,10); + +CREATE VARIABLE plpgsql_sesvar02 AS numeric; + +LET plpgsql_sesvar02 = 0.0; + +CREATE OR REPLACE FUNCTION svartest_plpgsql_inc(numeric) +RETURNS int AS $$ +BEGIN + LET plpgsql_sesvar02 = COALESCE(VARIABLE(plpgsql_sesvar02) + $1, $1); + RETURN VARIABLE(plpgsql_sesvar02); +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); +SELECT svartest_plpgsql_inc(1.0); + +SELECT svartest_plpgsql_inc(1.0) FROM generate_series(1,10); + +DROP VARIABLE plpgsql_sesvar01, plpgsql_sesvar02; + +DROP FUNCTION svartest_plpgsql_inc(int); +DROP FUNCTION svartest_plpgsql_inc(numeric); + +-- the value should not be corrupted +CREATE VARIABLE plpgsql_sesvar03 text; +LET plpgsql_sesvar03 = 'abc'; + +CREATE FUNCTION svartest_plpgsql_func03() +RETURNS text AS $$ +BEGIN + RETURN svartest_plpgsql_func_nested(VARIABLE(plpgsql_sesvar03)); +END +$$ LANGUAGE plpgsql; + +CREATE FUNCTION svartest_plpgsql_func_nested(t text) +RETURNS text AS $$ +BEGIN + LET plpgsql_sesvar03 = 'BOOM!'; + RETURN t; +END; +$$ LANGUAGE plpgsql; + +SELECT svartest_plpgsql_func03(); + +DROP FUNCTION svartest_plpgsql_func03(); +DROP FUNCTION svartest_plpgsql_func_nested(text); + +DROP VARIABLE plpgsql_sesvar03; -- 2.51.0 [text/x-patch] v20250929-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (52.3K, 6-v20250929-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch) download | inline diff: From 4cc23669637e1c4f602021b69afe259c646a85ce Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 08:29:37 +0200 Subject: [PATCH 11/15] LET command - assign a result of expression to the session variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The value is assigned to session variables usually by SET command. Unfortunately there are two reasons why SET should not be used for this purpose in Postgres. 1. Using a_expr inside generic_set ram rule produces reduce conflicts, so it needs total reimplementation of related gram rules. 2. SET is no plan command - so it doesn't support usage of parameters. 3. Excepting implementation issues, there is fact, so if we use SET command for assigning values to session variables, then there can be collisions between session variables and GUC, and then we need some concepts, how these collisions should be solved, or how to protect self against these collisions. With the dedicated command, the collisions between GUC and session variables are not possible. The command LET is executed as usual query execution. The result is stored to the target session variable (resultVariable) by using VariableDestReceiver. Implementations of EXPLAIN LET and PREPARE LET statements are not supported now. Postponed to next step due reducing patch size. --- doc/src/sgml/ddl.sgml | 29 ++ doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++++++ doc/src/sgml/reference.sgml | 1 + src/backend/commands/session_variable.c | 86 ++++++ src/backend/executor/execMain.c | 23 +- src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 24 ++ src/backend/optimizer/plan/setrefs.c | 34 ++- src/backend/parser/analyze.c | 237 +++++++++++++++ src/backend/parser/gram.y | 39 ++- src/backend/tcop/utility.c | 15 + src/backend/utils/cache/plancache.c | 11 + src/bin/psql/tab-complete.in.c | 12 +- src/include/commands/session_variable.h | 5 + src/include/nodes/parsenodes.h | 15 + src/include/nodes/pathnodes.h | 9 + src/include/nodes/plannodes.h | 7 + src/include/nodes/primnodes.h | 9 + src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 277 ++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 189 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 27 files changed, 1124 insertions(+), 15 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 2655fa5e7ce..bdf00b35508 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + </para> + + <para> + The value of a session variable is set with the SQL statement + <command>LET</command>. The value of a session variable can be retrieved + with the SQL statement <command>SELECT</command>. <programlisting> +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); +</programlisting> + + or + +<programlisting> +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); SELECT VARIABLE(current_user_id); </programlisting> </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a variable's value returns a <literal>NULL</literal>, unless its value has + been set to something else in the current session using the + <command>LET</command> command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + </para> </sect1> <sect1 id="ddl-others"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY grant SYSTEM "grant.sgml"> <!ENTITY importForeignSchema SYSTEM "import_foreign_schema.sgml"> <!ENTITY insert SYSTEM "insert.sgml"> +<!ENTITY let SYSTEM "let.sgml"> <!ENTITY listen SYSTEM "listen.sgml"> <!ENTITY load SYSTEM "load.sgml"> <!ENTITY lock SYSTEM "lock.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; <simplelist type="inline"> <member><xref linkend="sql-createvariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> </refentry> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..43000ce004d 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceab <title>Examples</title> <para> - Create an date session variable <literal>var1</literal>: + Create a session variable <literal>var1</literal> of data type date: <programlisting> CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); </programlisting> </para> @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-dropvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; <simplelist type="inline"> <member><xref linkend="sql-altervariable"/></member> <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-let"/></member> </simplelist> </refsect1> diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ +<!-- +doc/src/sgml/ref/let.sgml +PostgreSQL documentation +--> + +<refentry id="sql-let"> + <indexterm zone="sql-let"> + <primary>LET</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>changing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>LET</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>LET</refname> + <refpurpose>change a session variable's value</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +LET <replaceable class="parameter">session_variable</replaceable> = <replaceable class="parameter">sql_expression</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>LET</command> command assigns a value to the specified session + variable. + </para> + + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><replaceable class="parameter">session_variable</replaceable></term> + <listitem> + <para> + The name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">sql_expression</replaceable></term> + <listitem> + <para> + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> +<programlisting> +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); +</programlisting> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>LET</command> is a <productname>PostgreSQL</productname> + extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index dbc054795bb..768163e2009 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -19,15 +19,22 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" +#include "executor/execdesc.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/datum.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/snapmgr.h" #include "utils/syscache.h" /* @@ -514,3 +521,82 @@ CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) return variable; } + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 9e05afa3745..2d424a95889 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -234,13 +234,24 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) /* fill the array */ foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) { - AclResult aclresult; + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable (like base + * node of assignment indirect) we cannot do permission check, + * because we need read the value (and user can have only UPDATE + * variable). In this case the permission check is executed in + * write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; - aclresult = object_aclcheck(VariableRelationId, varid, - GetUserId(), ACL_SELECT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_VARIABLE, - get_session_variable_name(varid)); + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } estate->es_session_variables[i].value = GetSessionVariable(varid, diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index cd609c6e479..575365eefcd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4378,6 +4378,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 401f71d0073..ea16431e22b 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -352,6 +352,20 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->rel_notnullatts_hash = NULL; glob->sessionVariables = NIL; + /* + * The (session) result variable should be stored to global, because it is + * not set in subquery. When this variable is used other than in base + * node of assignment indirection, we need to check the access rights (and + * then we need to detect this situation). The variable used like base + * node cannot be different than target (result) variable. Because we know + * the result variable before planner invocation, we can simply search of + * usage just this variable, and we don't need to to wait until the end of + * planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; + /* * Assess whether it's feasible to use parallel mode for this query. We * can't do this in a standalone backend, or if the command will try to @@ -592,6 +606,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->sessionVariables = glob->sessionVariables; + /* + * The session variable used (and only used) like base node for assignemnt + * indirection should be excluded from permission check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 584cdf84e0f..b86ae00e9a2 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2230,6 +2230,27 @@ fix_param_node(PlannerInfo *root, Param *p) p->paramid = n; } + /* + * We do SELECT permission check of all variables used by the query + * excluding the variable that is used only as base node of assignment + * indirection. The variable id assigned to this param should be same + * like resultVariable id, and this param should be used only once in + * query. When the variable is referenced by any other param, we + * should to do SELECT permission check for this variable too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + return (Node *) p; } @@ -3715,7 +3736,7 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) /* * Record dependency on a session variable. The variable can be used as a - * session variable in an expression list. + * session variable in an expression list, or as the target of a LET statement. */ static void record_plan_variable_dependency(PlannerInfo *root, Oid varid) @@ -3817,9 +3838,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For + * those, we just need to transfer our attention to the contained + * query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3842,6 +3864,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 203734f20cd..507fcb6d62d 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -52,6 +52,7 @@ #include "utils/builtins.h" #include "utils/guc.h" #include "utils/rel.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" @@ -94,6 +95,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -341,6 +344,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -420,6 +424,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -471,6 +480,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -3317,6 +3327,233 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) return result; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special mark, + * because requires special access when we do SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for acquiring an + * AccessShareLock on target variable, setting plan dependency and finally + * for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * Produce a string representation of a LockClauseStrength value. * This should only be applied to valid values (not LCS_NONE). diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a9d69f2a1e9..441b6d03af5 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -296,7 +296,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -741,7 +741,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1088,6 +1088,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12916,6 +12917,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17991,6 +18024,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18605,6 +18639,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 1a0e11ba701..ab0400c1f7a 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0..c8e91be1482 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2053,6 +2053,17 @@ ScanQueryForLocks(Query *parsetree, bool acquire) query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 5fc5c15057b..49f77b7928f 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1262,8 +1262,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4783,6 +4783,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET <variable> with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 9f5c6e30fbd..2ebe8477789 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -17,11 +17,16 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" +#include "nodes/params.h" #include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" extern void SetSessionVariable(Oid varid, Datum value, bool isNull); extern Datum GetSessionVariable(Oid varid, bool *isNull); extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 9d966d3dd1d..d111e2d629a 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -147,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -2168,6 +2171,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index ed5127942e0..19f25578ba1 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -185,6 +185,15 @@ typedef struct PlannerGlobal /* list of used session variables */ List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 6ee92f33dab..7e05e75dfbb 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -152,6 +152,13 @@ typedef struct PlannedStmt /* OIDs for PARAM_VARIABLE Params */ List *sessionVariables; + /* + * The oid of session variable execluded from permission check. This + * session variable is used as base node of assignment indirection (and it + * is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 9d7b9f598f8..a3cfb1e8044 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -404,6 +404,15 @@ typedef struct Param Oid paramcollid; /* OID of used session variable or InvalidOid if none */ Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection (when + * target of LET statement is an array field or an record field). For this + * param we do not check SELECT access right, because this param is used + * just for execution of an modify operation. + */ + bool parambasenode; + /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 0ea0265de7c..8c0affba13b 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 3e21059acc2..67f78a548dc 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -189,3 +189,280 @@ drop cascades to table svartest_dml.testtab DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; +CREATE VARIABLE sesvar43 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); +-- should fail +LET sesvar43 = generate_series(1,2); +ERROR: expression returned more than one row +LET sesvar43 = generate_series(1,0); +ERROR: expression returned no rows +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; +SELECT svartest_dml.fx01(3.14); + fx01 +------ + +(1 row) + +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + fx02 | sesvar43 +------+---------- + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; +SELECT svartest_dml.fx03('Hello'); + fx03 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; +SELECT svartest_dml.fx04('Hello'); + fx04 +------- + Hello +(1 row) + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + row_to_json +------------------------ + {"a":10,"b":20,"c":30} +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45); + sesvar45 +------------ + (10,20,30) +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).*; + a | b | c +----+----+---- + 10 | 20 | 30 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45.a); + a +---- + 10 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar45).a; + a +---- + 10 +(1 row) + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +--------------------------------- + {"a":10,"b":20,"c":30,"d":null} +(1 row) + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------------ + {"a":100,"b":200,"c":300,"d":null} +(1 row) + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + a | b | c | d +-----+-----+-----+--- + 100 | 200 | 300 | +(1 row) + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + | | | +(1 row) + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); +SELECT * FROM svartest_dml.view01; + a | b | c | d +---+---+---+--- + 5 | 6 | 7 | 8 +(1 row) + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; +ERROR: cannot drop session variable svartest_dml.sesvar45 because other objects depend on it +DETAIL: view svartest_dml.view01 depends on session variable svartest_dml.sesvar45 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW svartest_dml.view01; +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET sesvar43 = 6.28; +SELECT svartest_dml.fx06(); + fx06 +------ + 6.28 +(1 row) + +LET sesvar43 = VARIABLE(sesvar43) * 2; +SELECT svartest_dml.fx06(); + fx06 +------- + 12.56 +(1 row) + +-- plan cache invalidation test +DROP VARIABLE sesvar43; +-- should fail +SELECT svartest_dml.fx06(); +ERROR: session variable "sesvar43" doesn't exist +LINE 1: VARIABLE(sesvar43) + ^ +QUERY: VARIABLE(sesvar43) +CONTEXT: PL/pgSQL function svartest_dml.fx06() line 3 at RETURN +CREATE VARIABLE sesvar43 AS numeric; +LET sesvar43 = 2.72; +SELECT svartest_dml.fx06(); + fx06 +------ + 2.72 +(1 row) + +DROP VARIABLE sesvar43; +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; +-- should fail +LET svartest_dml.sesvar46 = NULL; +ERROR: value for domain svartest_dml.int_not_null violates check constraint "int_not_null_check" +-- should be ok +LET svartest_dml.sesvar46 = 100; +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +----------------------------- + {"a":100,"b":2,"c":3,"d":4} +(1 row) + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); +ERROR: permission denied for session variable sesvar45 +-- should be ok +LET svartest_dml.sesvar45.b = 200; +SET ROLE TO DEFAULT; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + row_to_json +------------------------------- + {"a":100,"b":200,"c":3,"d":4} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; +SET ROLE TO regress_svartest_dml_write_only_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); +ERROR: permission denied for session variable sesvar47 +-- should be ok +LET svartest_dml.sesvar47[1] = 200; +SET ROLE TO DEFAULT; +SELECT VARIABLE(svartest_dml.sesvar47); + sesvar47 +----------- + {200,2,3} +(1 row) + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + sesvar48 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 10 +(1 row) + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + sesvar49 +---------- + 100 +(1 row) + +\close_prepared letps +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 14 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar44 +drop cascades to type svartest_dml.composite_type +drop cascades to session variable svartest_dml.sesvar45 +drop cascades to function svartest_dml.fx01(numeric) +drop cascades to function svartest_dml.fx02() +drop cascades to function svartest_dml.fx03(character varying) +drop cascades to function svartest_dml.fx04(character varying) +drop cascades to function svartest_dml.fx05(integer,integer,integer) +drop cascades to function svartest_dml.fx06() +drop cascades to type svartest_dml.int_not_null +drop cascades to session variable svartest_dml.sesvar46 +drop cascades to session variable svartest_dml.sesvar47 +drop cascades to session variable svartest_dml.sesvar48 +drop cascades to session variable svartest_dml.sesvar49 +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index b2870dde9e9..2c8d6c3a497 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -159,3 +159,192 @@ DROP ROLE regress_svartest_dml_read_role; DROP VARIABLE sesvar40; DROP TABLE svartest_dml; + +CREATE VARIABLE sesvar43 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET sesvar43 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET sesvar43 = generate_series(1,1); + +-- should fail +LET sesvar43 = generate_series(1,2); +LET sesvar43 = generate_series(1,0); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar44 AS varchar; +CREATE TYPE svartest_dml.composite_type AS (a int, b int, c int); +CREATE VARIABLE svartest_dml.sesvar45 AS svartest_dml.composite_type; + +CREATE OR REPLACE FUNCTION svartest_dml.fx01(numeric) +RETURNS void AS $$ +LET sesvar43 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION svartest_dml.fx02() +RETURNS numeric AS $$ +SELECT VARIABLE(sesvar43); +$$ LANGUAGE sql; + +SELECT svartest_dml.fx01(3.14); +SELECT svartest_dml.fx02(), VARIABLE(sesvar43); + +CREATE OR REPLACE FUNCTION svartest_dml.fx03(s varchar) +RETURNS varchar AS $$ +BEGIN + LET svartest_dml.sesvar44 = s; + RETURN VARIABLE(svartest_dml.sesvar44); +END +$$ LANGUAGE plpgsql; + +SELECT svartest_dml.fx03('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx04(s varchar) +RETURNS varchar AS $$ +BEGIN + LET sesvar44 = s; + RETURN VARIABLE(sesvar44); +END +$$ LANGUAGE plpgsql +SET SEARCH_PATH TO 'svartest_dml'; + +SELECT svartest_dml.fx04('Hello'); + +CREATE OR REPLACE FUNCTION svartest_dml.fx05(a int, b int, c int) +RETURNS svartest_dml.composite_type AS $$ +BEGIN + LET svartest_dml.sesvar45 = ROW(a, b, c); + RETURN VARIABLE(svartest_dml.sesvar45); +END; +$$ LANGUAGE plpgsql; + +SELECT row_to_json(svartest_dml.fx05(10, 20, 30)); + +SELECT VARIABLE(svartest_dml.sesvar45); +SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT VARIABLE(svartest_dml.sesvar45.a); +SELECT VARIABLE(svartest_dml.sesvar45).a; + +ALTER TYPE svartest_dml.composite_type ADD ATTRIBUTE d int; + +-- composite value should be still readable +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +LET svartest_dml.sesvar45 = ROW(100, 200, 300, NULL); +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +-- use variables inside view +CREATE VIEW svartest_dml.view01 AS SELECT VARIABLE(svartest_dml.sesvar45).*; +SELECT * FROM svartest_dml.view01; + +-- start new connection +\c +SELECT * FROM svartest_dml.view01; + +LET svartest_dml.sesvar45 = ROW(5, 6, 7, 8); + +SELECT * FROM svartest_dml.view01; + +-- should fail (dependency) +DROP VARIABLE svartest_dml.sesvar45; + +DROP VIEW svartest_dml.view01; + +-- test of access variables from generic plans +CREATE OR REPLACE FUNCTION svartest_dml.fx06() +RETURNS numeric AS $$ +BEGIN + RETURN VARIABLE(sesvar43); +END; +$$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET sesvar43 = 6.28; + +SELECT svartest_dml.fx06(); + +LET sesvar43 = VARIABLE(sesvar43) * 2; + +SELECT svartest_dml.fx06(); + +-- plan cache invalidation test +DROP VARIABLE sesvar43; + +-- should fail +SELECT svartest_dml.fx06(); + +CREATE VARIABLE sesvar43 AS numeric; + +LET sesvar43 = 2.72; + +SELECT svartest_dml.fx06(); + +DROP VARIABLE sesvar43; + +CREATE DOMAIN svartest_dml.int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.sesvar46 AS svartest_dml.int_not_null; + +-- should fail +LET svartest_dml.sesvar46 = NULL; +-- should be ok +LET svartest_dml.sesvar46 = 100; + +LET svartest_dml.sesvar45 = ROW(1,2,3,4); +LET svartest_dml.sesvar45.a = 100; +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE ROLE regress_svartest_dml_write_only_role; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_write_only_role; +GRANT UPDATE ON VARIABLE svartest_dml.sesvar45 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar45); + +-- should be ok +LET svartest_dml.sesvar45.b = 200; + +SET ROLE TO DEFAULT; + +SELECT row_to_json(VARIABLE(svartest_dml.sesvar45)); + +CREATE VARIABLE svartest_dml.sesvar47 AS int[]; +LET svartest_dml.sesvar47 = ARRAY[1,2,3]; + +GRANT UPDATE ON VARIABLE svartest_dml.sesvar47 TO regress_svartest_dml_write_only_role; + +SET ROLE TO regress_svartest_dml_write_only_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar47); + +-- should be ok +LET svartest_dml.sesvar47[1] = 200; + +SET ROLE TO DEFAULT; + +SELECT VARIABLE(svartest_dml.sesvar47); + +CREATE VARIABLE svartest_dml.sesvar48 AS int4multirange[]; +LET svartest_dml.sesvar48 = NULL; +LET svartest_dml.sesvar48 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET svartest_dml.sesvar48[2] = '{[5,8),[12,100)}'; +SELECT VARIABLE(svartest_dml.sesvar48); + +-- test extended query protocol +CREATE VARIABLE svartest_dml.sesvar49 AS int; + +LET svartest_dml.sesvar49 = $1 \bind 10 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +LET svartest_dml.sesvar49 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(svartest_dml.sesvar49); + +\close_prepared letps + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_write_only_role; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6501516c086..20a44685879 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1546,6 +1546,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey -- 2.51.0 [text/x-patch] v20250929-0013-DISCARD-VARIABLES.patch (10.2K, 7-v20250929-0013-DISCARD-VARIABLES.patch) download | inline diff: From 0cad65393f646b7d92cf850f4c9c09018f7b1928 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Mon, 2 Jun 2025 20:41:57 +0200 Subject: [PATCH 13/15] DISCARD VARIABLES Implementation of DISCARD VARIABLES commands by removing hash table with session variables and resetting related memory context. --- doc/src/sgml/ref/discard.sgml | 13 ++++- src/backend/commands/discard.c | 6 +++ src/backend/commands/session_variable.c | 32 ++++++++++-- src/backend/parser/gram.y | 6 +++ src/backend/tcop/utility.c | 3 ++ src/bin/psql/tab-complete.in.c | 2 +- src/include/commands/session_variable.h | 2 + src/include/nodes/parsenodes.h | 1 + src/include/tcop/cmdtaglist.h | 1 + .../expected/session_variables_dml.out | 50 +++++++++++++++++++ .../regress/sql/session_variables_dml.sql | 24 +++++++++ 11 files changed, 135 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac..61b967f9c9b 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } </synopsis> </refsynopsisdiv> @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } </listitem> </varlistentry> + <varlistentry> + <term><literal>VARIABLES</literal></term> + <listitem> + <para> + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to <literal>NULL</literal>. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>TEMPORARY</literal> or <literal>TEMP</literal></term> <listitem> @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; </programlisting></para> </listitem> </varlistentry> diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a52..5904a6c4917 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/session_variable.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSessionVariables(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index 73ebf0340e3..52126a10f62 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -103,7 +103,13 @@ pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); - Assert(sessionvars); + /* + * There is no guarantee of session variables being initialized, even when + * receiving an invalidation callback, as DISCARD [ ALL | VARIABLES ] + * destroys the hash table entirely. + */ + if (!sessionvars) + return; /* * If the hashvalue is not specified, we have to recheck all currently @@ -657,8 +663,8 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) /* * It is also possible that a variable has been dropped and - * someone created a new variable with the same object ID. - * Use the catalog information only if that is not the case. + * someone created a new variable with the same object ID. Use + * the catalog information only if that is not the case. */ if (svar->create_lsn == varform->varcreate_lsn) { @@ -700,3 +706,23 @@ pg_get_session_variables_memory(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * Fast drop of the complete content of the session variables hash table, and + * cleanup of any list that wouldn't be relevant anymore. + * This is used by the DISCARD VARIABLES (and DISCARD ALL) command. + */ +void +ResetSessionVariables(void) +{ + /* destroy hash table and reset related memory context */ + if (sessionvars) + { + hash_destroy(sessionvars); + sessionvars = NULL; + } + + /* release memory allocated by session variables */ + if (SVariableMemoryContext != NULL) + MemoryContextReset(SVariableMemoryContext); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 441b6d03af5..f606f392070 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2119,7 +2119,13 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index ab0400c1f7a..7a9271d0639 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 49f77b7928f..dfd1973be38 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -4210,7 +4210,7 @@ match_previous_words(int pattern_id, /* DISCARD */ else if (Matches("DISCARD")) - COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP"); + COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP", "VARIABLES"); /* DO */ else if (Matches("DO")) diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 2ebe8477789..ac36dfcc19b 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -29,4 +29,6 @@ extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *st extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, QueryEnvironment *queryEnv, QueryCompletion *qc); +extern void ResetSessionVariables(void); + #endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d111e2d629a..4efdc1bfeb0 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4104,6 +4104,7 @@ typedef enum DiscardMode DISCARD_PLANS, DISCARD_SEQUENCES, DISCARD_TEMP, + DISCARD_VARIABLES, } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index 22082c30008..bef0ac25331 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -135,6 +135,7 @@ PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false) PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false) +PG_CMDTAG(CMDTAG_DISCARD_VARIABLES, "DISCARD VARIABLES", false, false, false) PG_CMDTAG(CMDTAG_DO, "DO", false, false, false) PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false) PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false) diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out index 67f78a548dc..dfbe7900c1c 100644 --- a/src/test/regress/expected/session_variables_dml.out +++ b/src/test/regress/expected/session_variables_dml.out @@ -466,3 +466,53 @@ drop cascades to session variable svartest_dml.sesvar47 drop cascades to session variable svartest_dml.sesvar48 drop cascades to session variable svartest_dml.sesvar49 DROP ROLE regress_svartest_dml_write_only_role; +CREATE SCHEMA svartest_dml_discard; +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + Hello +(1 row) + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + count +------- + 1 +(1 row) + +DISCARD ALL; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 1 +(1 row) + +DISCARD VARIABLES; +SELECT count(*) FROM pg_get_session_variables_memory(); + count +------- + 0 +(1 row) + +SELECT VARIABLE(svartest_dml_discard.sesvar50); + sesvar50 +---------- + +(1 row) + +DROP SCHEMA svartest_dml_discard CASCADE; +NOTICE: drop cascades to session variable svartest_dml_discard.sesvar50 diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql index 2c8d6c3a497..b0aeeb865c2 100644 --- a/src/test/regress/sql/session_variables_dml.sql +++ b/src/test/regress/sql/session_variables_dml.sql @@ -348,3 +348,27 @@ SELECT VARIABLE(svartest_dml.sesvar49); DROP SCHEMA svartest_dml CASCADE; DROP ROLE regress_svartest_dml_write_only_role; + +CREATE SCHEMA svartest_dml_discard; + +CREATE VARIABLE svartest_dml_discard.sesvar50 AS varchar; +LET svartest_dml_discard.sesvar50 = 'Hello'; +SELECT VARIABLE(svartest_dml_discard.sesvar50); + +SELECT count(*) FROM pg_get_session_variables_memory() WHERE schema = 'svartest_dml_discard'; + +DISCARD ALL; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +LET svartest_dml_discard.sesvar50 = 'Hello'; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +DISCARD VARIABLES; + +SELECT count(*) FROM pg_get_session_variables_memory(); + +SELECT VARIABLE(svartest_dml_discard.sesvar50); +DROP SCHEMA svartest_dml_discard CASCADE; -- 2.51.0 [text/x-patch] v20250929-0010-svariableReceiver.patch (8.9K, 8-v20250929-0010-svariableReceiver.patch) download | inline diff: From 0107dd18a3188c944f02e35154608227916336e0 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 21:20:16 +0200 Subject: [PATCH 10/15] svariableReceiver allows to store result of the query to session variable Check correct format of result - one column, one row. --- src/backend/executor/Makefile | 1 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++++++++++++++++++++++ src/backend/tcop/dest.c | 7 + src/include/executor/svariableReceiver.h | 22 +++ src/include/tcop/dest.h | 1 + src/tools/pgindent/typedefs.list | 1 + 7 files changed, 205 insertions(+) create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/executor/svariableReceiver.h diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..41a967cb4b2 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9c719841c28..6501516c086 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2658,6 +2658,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250929-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20250929-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch) download | inline diff: From 183dfc61d24181d59333c66578427d6014bfb3d5 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Sun, 1 Jun 2025 07:32:01 +0200 Subject: [PATCH 09/15] fill an auxiliary buffer with values of session variables used in query and locks variables used in query. Now we can read the content of any session variable. Direct reading from expression executor is not allowed, so we cannot to use session variables inside CALL or EXECUTE commands (can be supported with direct access to session variables (from expression executor) - postponed). Using session variables blocks parallel query execution. It is not technical problem (it just needs a serialization/deserialization of es_session_varibles buffer), but it increases a size of patch (and then it is postponed). --- src/backend/executor/execExpr.c | 29 +++ src/backend/executor/execMain.c | 56 +++++ src/backend/utils/cache/plancache.c | 24 ++- src/include/nodes/execnodes.h | 14 ++ .../expected/session_variables_dml.out | 191 ++++++++++++++++++ src/test/regress/parallel_schedule | 7 +- .../regress/sql/session_variables_dml.sql | 161 +++++++++++++++ src/tools/pgindent/typedefs.list | 1 + 8 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/session_variables_dml.out create mode 100644 src/test/regress/sql/session_variables_dml.sql diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..0457a729537 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1069,6 +1069,35 @@ ExecInitExprRec(Expr *node, ExprState *state, ExprEvalPushStep(state, &scratch); } break; + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; default: elog(ERROR, "unrecognized paramkind: %d", (int) param->paramkind); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 831c55ce787..9e05afa3745 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +198,60 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this + * array is passed to the executor. This array is stable "snapshot" of + * values of used session variables. There are three benefits of this + * strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable (this + * is necessary because the internally the session variable can be changed + * inside query execution time, and then a reference to previously + * returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index ab1f2af13e5..1d20f3382b0 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -2043,9 +2043,12 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); @@ -2053,7 +2056,8 @@ ScanQueryForLocks(Query *parsetree, bool acquire) } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2068,6 +2072,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index a36653c37f9..43cd47b5280 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -645,6 +645,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -704,6 +714,10 @@ typedef struct EState ParamListInfo es_param_list_info; /* values of external params */ ParamExecData *es_param_exec_vals; /* values of internal params */ + /* Session variables info: */ + int es_num_session_variables; /* number of used variables */ + SessionVariableValue *es_session_variables; /* array of copies of values */ + QueryEnvironment *es_queryEnv; /* query environment */ /* Other working state: */ diff --git a/src/test/regress/expected/session_variables_dml.out b/src/test/regress/expected/session_variables_dml.out new file mode 100644 index 00000000000..3e21059acc2 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,191 @@ +CREATE VARIABLE sesvar40 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; +ERROR: column "sesvar40" does not exist +LINE 1: SELECT sesvar40; + ^ +-- should be ok +SELECT VARIABLE(sesvar40); + sesvar40 +---------- + +(1 row) + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); +SELECT sesvar41 FROM svartest_dml; -- 100 + sesvar41 +---------- + 100 +(1 row) + +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + sesvar41 +---------- + +(1 row) + +-- should fail +SELECT VARIABLE(sesvar41); +ERROR: session variable "sesvar41" doesn't exist +LINE 1: SELECT VARIABLE(sesvar41); + ^ +SET SEARCH_PATH TO svartest_dml; +-- should be ok +SELECT VARIABLE(sesvar41); + sesvar41 +---------- + +(1 row) + +SET SEARCH_PATH TO DEFAULT; +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; +SELECT svartest_dml.testsql(); + testsql +--------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + ^ +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41... + ^ +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); +ERROR: value for domain svartest_dml_int_not_null violates check constraint "svartest_dml_int_not_null_check" +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; +CREATE ROLE regress_svartest_dml_read_role; +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); +ERROR: permission denied for session variable sesvar41 +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +ERROR: permission denied for session variable sesvar41 +CONTEXT: PL/pgSQL expression "VARIABLE(svartest_dml.sesvar41)" +PL/pgSQL function inline_code_block line 3 at RAISE +-- using sql function should to fail +SELECT svartest_dml.testsql(); +ERROR: permission denied for session variable sesvar41 +CONTEXT: SQL function "testsql" statement 1 +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + func_secdef +------------- + +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION svartest_dml.func_secdef(); +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; +SET ROLE TO regress_svartest_dml_read_role; +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + sesvar41 +---------- + +(1 row) + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; +NOTICE: <NULL> +SET ROLE TO DEFAULT; +CREATE TABLE svartest_dml.testtab(a int); +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); +ANALYZE svartest_dml.testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using svartest_dml_testtab_a on testtab + Index Cond: (a = VARIABLE(sesvar40)) +(2 rows) + +DROP INDEX svartest_dml.svartest_dml_testtab_a; +SET enable_seqscan TO DEFAULT; +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + QUERY PLAN +------------------------------------ + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + QUERY PLAN +------------------------------------ + Seq Scan on testtab + Filter: (a = VARIABLE(sesvar40)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP SCHEMA svartest_dml CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to session variable svartest_dml.sesvar41 +drop cascades to function svartest_dml.testsql() +drop cascades to table svartest_dml.testtab +DROP ROLE regress_svartest_dml_read_role; +DROP VARIABLE sesvar40; +DROP TABLE svartest_dml; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 95c76baf9ae..b16ec065950 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml # ---------- # Another group of parallel tests @@ -140,3 +140,8 @@ test: fast_default # run tablespace test at the end because it drops the tablespace created during # setup that other tests may use. test: tablespace + +# ---------- +# Another group of parallel tests (session variables related) +# ---------- +test: session_variables_ddl session_variables_acl session_variables_dml diff --git a/src/test/regress/sql/session_variables_dml.sql b/src/test/regress/sql/session_variables_dml.sql new file mode 100644 index 00000000000..b2870dde9e9 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,161 @@ +CREATE VARIABLE sesvar40 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT sesvar40; + +-- should be ok +SELECT VARIABLE(sesvar40); + +CREATE SCHEMA svartest_dml; +CREATE VARIABLE svartest_dml.sesvar41 AS int; + +-- identifier collision test +CREATE TABLE svartest_dml(sesvar41 int); +INSERT INTO svartest_dml VALUES(100); + +SELECT sesvar41 FROM svartest_dml; -- 100 +SELECT VARIABLE(svartest_dml.sesvar41); -- NULL + +-- should fail +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO svartest_dml; + +-- should be ok +SELECT VARIABLE(sesvar41); + +SET SEARCH_PATH TO DEFAULT; + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +CREATE OR REPLACE FUNCTION svartest_dml.testsql() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE sql; + +SELECT svartest_dml.testsql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE svartest_dml.proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL svartest_dml.proc(VARIABLE(svartest_dml.sesvar41)); + +PREPARE svartest_dml_prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE svartest_dml_prepstmt(VARIABLE(svartest_dml.sesvar41)); + +DROP PROCEDURE svartest_dml.proc; +DEALLOCATE svartest_dml_prepstmt; + +-- domains are supported +CREATE DOMAIN svartest_dml_int_not_null AS int CHECK(value IS NOT NULL); +CREATE VARIABLE svartest_dml.svartest42 AS svartest_dml_int_not_null; + +-- should fail +SELECT VARIABLE(svartest_dml.svartest42); + +DROP VARIABLE svartest_dml.svartest42; +DROP DOMAIN svartest_dml_int_not_null; + +CREATE ROLE regress_svartest_dml_read_role; + +CREATE OR REPLACE FUNCTION svartest_dml.func_secdef() +RETURNS int AS $$ +SELECT VARIABLE(svartest_dml.sesvar41); +$$ LANGUAGE SQL SECURITY DEFINER; + +GRANT USAGE ON SCHEMA svartest_dml TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should fail +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should fail +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +-- using sql function should to fail +SELECT svartest_dml.testsql(); + +-- using security definer should be ok +SELECT svartest_dml.func_secdef(); + +SET ROLE TO DEFAULT; + +DROP FUNCTION svartest_dml.func_secdef(); + +GRANT SELECT ON VARIABLE svartest_dml.sesvar41 TO regress_svartest_dml_read_role; + +SET ROLE TO regress_svartest_dml_read_role; + +-- should be ok +SELECT VARIABLE(svartest_dml.sesvar41); + +-- should be ok +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(svartest_dml.sesvar41); +END; +$$; + +SET ROLE TO DEFAULT; + +CREATE TABLE svartest_dml.testtab(a int); + +INSERT INTO svartest_dml.testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX svartest_dml_testtab_a ON svartest_dml.testtab(a); + +ANALYZE svartest_dml.testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +DROP INDEX svartest_dml.svartest_dml_testtab_a; + +SET enable_seqscan TO DEFAULT; + +-- parallel execution should be blocked +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- parallel plan should be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM svartest_dml.testtab WHERE a = VARIABLE(sesvar40); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP SCHEMA svartest_dml CASCADE; +DROP ROLE regress_svartest_dml_read_role; + +DROP VARIABLE sesvar40; + +DROP TABLE svartest_dml; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 5d89ab0d76e..9c719841c28 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2716,6 +2716,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData -- 2.51.0 [text/x-patch] v20250929-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.5K, 10-v20250929-0008-collect-session-variables-used-in-plan-and-assign-pa.patch) download | inline diff: From 007d85f29fd96d0f03475ff4bd1cce6a526a977c Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 09:31:06 +0200 Subject: [PATCH 08/15] collect session variables used in plan and assign paramid In the plan stage we need to collect used session variables. On the order of this list, the param nodes gets paramid (fix_param_node). This number is used (later) as index to buffer of values of the used session variables. The buffer is prepared and filled by executor. Some unsupported optimizations are disabled: * parallel execution * simple expression execution in PL/pgSQL * SQL functions inlining Before execution of query with session variables we need to collect used session variables. This list is used for --- doc/src/sgml/parallel.sgml | 6 ++ src/backend/catalog/dependency.c | 5 + src/backend/optimizer/plan/planner.c | 11 ++ src/backend/optimizer/plan/setrefs.c | 124 +++++++++++++++++++++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +++++- src/backend/utils/cache/plancache.c | 6 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/include/nodes/pathnodes.h | 5 + src/include/nodes/plannodes.h | 3 + src/include/optimizer/planmain.h | 2 + 11 files changed, 199 insertions(+), 9 deletions(-) diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated <literal>SubPlan</literal>. </para> </listitem> + + <listitem> + <para> + Plan nodes that use a session variable. + </para> + </listitem> </itemizedlist> <sect2 id="parallel-labeling"> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7..eb91b46b128 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1875,6 +1875,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 41bd8353430..401f71d0073 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -350,6 +350,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->dependsOnRole = false; glob->partition_directory = NULL; glob->rel_notnullatts_hash = NULL; + glob->sessionVariables = NIL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -588,6 +589,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + + result->sessionVariables = glob->sessionVariables; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -770,6 +774,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 6950eff2c5b..584cdf84e0f 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1322,6 +1325,50 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * Search usage of session variables in subqueries + */ +void +pull_up_has_session_variables(PlannerInfo *root) +{ + Query *query = root->parse; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + } + else + { + (void) query_tree_walker(query, + pull_up_has_session_variables_walker, + (void *) root, 0); + } +} + +static bool +pull_up_has_session_variables_walker(Node *node, PlannerInfo *root) +{ + if (node == NULL) + return false; + if (IsA(node, Query)) + { + Query *query = (Query *) node; + + if (query->hasSessionVariables) + { + root->hasSessionVariables = true; + return false; + } + + /* recurse into subselects */ + return query_tree_walker((Query *) node, + pull_up_has_session_variables_walker, + (void *) root, 0); + } + return expression_tree_walker(node, pull_up_has_session_variables_walker, + (void *) root); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2022,8 +2069,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2113,6 +2161,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2122,6 +2177,10 @@ fix_expr_common(PlannerInfo *root, Node *node) * If it's a PARAM_MULTIEXPR, replace it with the appropriate Param from * root->multiexpr_params; otherwise no change is needed. * Just for paranoia's sake, we make a copy of the node in either case. + * + * If it's a PARAM_VARIABLE, then we collect used session variables in + * the list root->glob->sessionVariable. Also, assign the parameter's + * "paramid" to the parameter's position in that list. */ static Node * fix_param_node(PlannerInfo *root, Param *p) @@ -2140,6 +2199,40 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2201,7 +2294,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2223,7 +2319,8 @@ fix_scan_expr(PlannerInfo *root, Node *node, int rtoffset, double num_exec) root->multiexpr_params != NIL || root->glob->lastPHId != 0 || root->minmax_aggs != NIL || - root->hasAlternativeSubPlans) + root->hasAlternativeSubPlans || + root->hasSessionVariables) { return fix_scan_expr_mutator(node, &context); } @@ -3616,6 +3713,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 35e8d3c183b..d2470c9b2db 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1645,6 +1645,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* If subquery had any RLS conditions, now main query does too */ parse->hasRowSecurity |= subquery->hasRowSecurity; + /* if the subquery had session variables, the main query does too */ + parse->hasSessionVariables |= subquery->hasSessionVariables; + /* * subquery won't be pulled up if it hasAggs, hasWindowFuncs, or * hasTargetSRFs, so no work needed on those flags diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index f49bde7595b..3941018903e 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -25,6 +25,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -939,6 +940,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2397,6 +2405,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2523,6 +2532,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4821,7 +4851,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6661d2c6b73..ab1f2af13e5 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -153,6 +154,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2196,7 +2198,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index b4c1e2c4b21..de1ffeed317 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -1990,9 +1990,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index b12a2508d8c..ed5127942e0 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,9 @@ typedef struct PlannerGlobal /* hash table for NOT NULL attnums of relations */ struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -526,6 +529,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3d196f5078e..6ee92f33dab 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -149,6 +149,9 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* OIDs for PARAM_VARIABLE Params */ + List *sessionVariables; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab28..ba4305d61a7 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ -- 2.51.0 [text/x-patch] v20250929-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20250929-0007-local-HASHTAB-for-currently-used-session-variables-a.patch) download | inline diff: From 0d6dd7a08a7528f0acba9187628da50b4205a67b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 22:44:58 +0200 Subject: [PATCH 07/15] local HASHTAB for currently used session variables and low level access functions Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. Hash entries related to dropped session variables are not released. The memory cleaning is implemented in memory-cleaning-after-DROP-VARIABLE patch. --- doc/src/sgml/glossary.sgml | 5 +- src/backend/commands/session_variable.c | 428 ++++++++++++++++++++++++ src/include/commands/session_variable.h | 3 + src/tools/pgindent/typedefs.list | 2 + 4 files changed, 436 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index c37fd5da50b..97cd13957bb 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1717,8 +1717,9 @@ <para> A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. </para> <para> For more information, see <xref linkend="ddl-session-variables"/>. diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1ac..dbc054795bb 100644 --- a/src/backend/commands/session_variable.c +++ b/src/backend/commands/session_variable.c @@ -14,14 +14,442 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_variable.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" +#include "storage/proc.h" #include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory + * context for session variables, but we can use TopTransactionContext + * instead. A fresh value is forced when we detect we are in a different + * transaction (the local transaction ID differs from + * domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash + * bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got the + * same OID. We do a second check against the 64-bit LSN when the + * variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still + * be valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain + * check. But the variable and all its dependencies are locked now, so we + * don't need to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} /* * Creates a new variable diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h index 49f36ac6885..9f5c6e30fbd 100644 --- a/src/include/commands/session_variable.h +++ b/src/include/commands/session_variable.h @@ -19,6 +19,9 @@ #include "parser/parse_node.h" #include "nodes/parsenodes.h" +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); #endif diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 60e55a8058c..5d89ab0d76e 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2656,6 +2656,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan -- 2.51.0 [text/x-patch] v20250929-0006-session-variable-fences-parsing.patch (32.1K, 12-v20250929-0006-session-variable-fences-parsing.patch) download | inline diff: From 252ae01dea874dab670917eb4b21831a7f427b3a Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Fri, 30 May 2025 07:27:27 +0200 Subject: [PATCH 06/15] session variable fences parsing The session variables can be used in query only inside the variable fence. This is special syntax VARIABLE(varname), that eliminates a risk of collision between variable and column identifier. The session variables cannot be used as parameters of CALL or EXECUTE commands. These commands evaluates arguments by direct call of expression executor, and direct access to session variables from expression executor will be implemented later (in next step). --- doc/src/sgml/ddl.sgml | 11 ++ src/backend/catalog/namespace.c | 289 ++++++++++++++++++++++++++++ src/backend/commands/prepare.c | 8 + src/backend/nodes/nodeFuncs.c | 6 + src/backend/parser/analyze.c | 7 + src/backend/parser/gram.y | 28 ++- src/backend/parser/parse_expr.c | 208 +++++++++++++++++++- src/backend/parser/parse_merge.c | 1 + src/backend/parser/parse_target.c | 7 + src/backend/utils/adt/ruleutils.c | 46 +++++ src/backend/utils/cache/lsyscache.c | 24 +++ src/include/catalog/namespace.h | 2 + src/include/nodes/parsenodes.h | 12 ++ src/include/nodes/primnodes.h | 5 + src/include/parser/parse_node.h | 1 + src/include/utils/lsyscache.h | 4 + src/pl/plpgsql/src/pl_exec.c | 3 +- src/tools/pgindent/typedefs.list | 1 + 18 files changed, 659 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 420a4d9ff11..2655fa5e7ce 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. </para> + + <para> + In an query the session variable can be used only inside + <firstterm>variable fence</firstterm>. This is special syntax for + session variable identifier, and can be used only for session variable + identifier. The special syntax for accessing session variables removes + risk of collisions between variable identifiers and column names. +<programlisting> +SELECT VARIABLE(current_user_id); +</programlisting> + </para> </sect1> <sect1 id="ddl-others"> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 338d78e174f..e7e767039c5 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3564,6 +3564,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a2..fcadcd9bc3f 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -341,6 +341,14 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression executor + * call. This mode doesn't support session variables yet. It will be + * enabled later. This case should be blocked parser by + * expr_kind_allows_session_variables, so only assertions is used here. + */ + Assert(!pstate->p_hasSessionVariables); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cd609c6e479 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1673,6 +1673,9 @@ exprLocation(const Node *expr) case T_ParamRef: loc = ((const ParamRef *) expr)->location; break; + case T_VariableFence: + loc = ((const VariableFence *) expr)->location; + break; case T_A_Const: loc = ((const A_Const *) expr)->location; break; @@ -4705,6 +4708,9 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_VariableFence: + /* we assume the fields contain nothing interesting */ + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 5aeb54eb5f6..203734f20cd 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -618,6 +618,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1043,6 +1044,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1524,6 +1526,7 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt, qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, stmt->lockingClause) { @@ -1750,6 +1753,7 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2001,6 +2005,7 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; foreach(l, lockingClause) { @@ -2473,6 +2478,7 @@ transformReturnStmt(ParseState *pstate, ReturnStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -2540,6 +2546,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 6926cea75b9..a9d69f2a1e9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -525,7 +525,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type <node> def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref having_clause func_table xmltable array_expr - OptWhereClause operator_def_arg + OptWhereClause operator_def_arg variable_fence %type <list> opt_column_and_period_list %type <list> rowsfrom_item rowsfrom_list opt_col_def_list %type <boolean> opt_ordinality opt_without_overlaps @@ -881,7 +881,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH + SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' @@ -15691,6 +15691,19 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence opt_indirection + { + if ($2) + { + A_Indirection *n = makeNode(A_Indirection); + + n->arg = (Node *) $1; + n->indirection = check_indirection($2, yyscanner); + $$ = (Node *) n; + } + else + $$ = $1; + } | case_expr { $$ = $1; } | func_expr @@ -17066,6 +17079,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' any_name ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e1979a80c19..d17d0583cd4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -15,6 +15,7 @@ #include "postgres.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -76,6 +77,7 @@ static Node *transformWholeRowRef(ParseState *pstate, static Node *transformIndirection(ParseState *pstate, A_Indirection *ind); static Node *transformTypeCast(ParseState *pstate, TypeCast *tc); static Node *transformCollateClause(ParseState *pstate, CollateClause *c); +static Node *transformVariableFence(ParseState *pstate, VariableFence *vf); static Node *transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor); static Node *transformJsonArrayConstructor(ParseState *pstate, @@ -105,7 +107,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); - +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* * transformExpr - @@ -369,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -902,6 +910,135 @@ transformParamRef(ParseState *pstate, ParamRef *pref) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_POLICY: + case EXPR_KIND_COPY_WHERE: + result = false; + break; + } + + return result; +} + +static Node * +transformVariableFence(ParseState *pstate, VariableFence *vf) +{ + Node *result; + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* VariableFence can be used only in context when variables are supported */ + if (!expr_kind_allows_session_variables(pstate->p_expr_kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable reference is not supported here"), + parser_errposition(pstate, vf->location))); + + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(vf->varname, &attrname, ¬_unique, false); + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + result = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, vf->location); + } + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(vf->varname)), + parser_errposition(pstate, vf->location))); + + return result; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -3121,6 +3258,75 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg) return (Node *) nt; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + /* * Produce a string identifying an expression by kind. * diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 905c975d83b..8b5240cd54b 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2033,6 +2033,13 @@ FigureColnameInternal(Node *node, char **name) (int) ((JsonFuncExpr *) node)->op); } break; + case T_VariableFence: + { + /* return last field name */ + *name = strVal(llast(((VariableFence *) node)->varname)); + return 2; + } + break; default: break; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index defcdaa8b34..f6016020938 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8802,6 +8804,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * @@ -13567,6 +13577,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 1c4031eea23..b9b0ac55475 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3946,3 +3946,27 @@ get_session_variable_namespace(Oid varid) return varnamespace; } + +/* + * Returns the type, typmod and collid of the given session variable. + */ +void +get_session_variable_type_typmod_collid(Oid varid, Oid *typid, int32 *typmod, + Oid *collid) +{ + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + *typid = varform->vartype; + *typmod = varform->vartypmod; + *collid = varform->varcollation; + + ReleaseSysCache(tup); +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index d12c3b957b7..5aa86b1db71 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -116,6 +116,8 @@ extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); extern bool VariableIsVisible(Oid varid); +extern Oid IdentifyVariable(List *names, char **attrname, + bool *not_unique, bool noerror); extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 464cf782b8d..9d966d3dd1d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -167,6 +167,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -321,6 +323,16 @@ typedef struct ParamRef ParseLoc location; /* token location, or -1 if unknown */ } ParamRef; +/* + * VariableFence - ensure so fields will be interpretted as a variable + */ +typedef struct VariableFence +{ + NodeTag type; + List *varname; /* variable name (String nodes) */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..9d7b9f598f8 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,8 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +387,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -399,6 +402,8 @@ typedef struct Param int32 paramtypmod; /* OID of collation, or InvalidOid if none */ Oid paramcollid; + /* OID of used session variable or InvalidOid if none */ + Oid paramvarid pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c84542..84e886940d8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -228,6 +228,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 03c4c58580e..5926a854d12 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -214,6 +214,10 @@ extern char *get_subscription_name(Oid subid, bool missing_ok); extern char *get_session_variable_name(Oid varid); extern Oid get_session_variable_namespace(Oid varid); +extern void get_session_variable_type_typmod_collid(Oid varid, + Oid *typid, + int32 *typmod, + Oid *collid); #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index d19425b7a71..96857874ffe 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8268,7 +8268,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9fb3909f7f6..60e55a8058c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3204,6 +3204,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState -- 2.51.0 [text/x-patch] v20250929-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 13-v20250929-0005-support-of-session-variables-for-pg_dump.patch) download | inline diff: From 8fc61f6ae45c54a63631dee83465c0613f9adf6f Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 22:54:09 +0200 Subject: [PATCH 05/15] support of session variables for pg_dump This patch enhancing pg_dump to support session variables. --- src/bin/pg_dump/common.c | 3 + src/bin/pg_dump/dumputils.c | 6 + src/bin/pg_dump/pg_backup.h | 2 + src/bin/pg_dump/pg_backup_archiver.c | 9 ++ src/bin/pg_dump/pg_dump.c | 190 +++++++++++++++++++++++++++ src/bin/pg_dump/pg_dump.h | 15 +++ src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/pg_dump/t/002_pg_dump.pl | 63 +++++++++ src/tools/pgindent/typedefs.list | 1 + 9 files changed, 296 insertions(+) diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index a1976fae607..5333acb7bbd 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -247,6 +247,9 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscription membership of tables"); getSubscriptionTables(fout); + pg_log_info("reading variables"); + getVariables(fout); + free(inhinfo); /* not needed any longer */ *numTablesPtr = numTables; diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 05b84c0d6e7..15cbe5616fa 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -552,6 +552,12 @@ do { \ CONVERT_PRIV('r', "SELECT"); CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "VARIABLE") == 0 || + strcmp(type, "VARIABLES") == 0) + { + CONVERT_PRIV('r', "SELECT"); + CONVERT_PRIV('w', "UPDATE"); + } else abort(); diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index d9041dad720..dac067bdc4c 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,12 +134,14 @@ typedef struct _restoreOptions int selFunction; int selTrigger; int selTable; + int selVariable; SimpleStringList indexNames; SimpleStringList functionNames; SimpleStringList schemaNames; SimpleStringList schemaExcludeNames; SimpleStringList triggerNames; SimpleStringList tableNames; + SimpleStringList variableNames; int useDB; ConnParams cparams; /* parameters to use if useDB */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 59eaecb4ed7..06774dc75c1 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3241,6 +3241,14 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) !simple_string_list_member(&ropt->triggerNames, te->tag)) return 0; } + else if (strcmp(te->desc, "VARIABLE") == 0) + { + if (!ropt->selVariable) + return 0; + if (ropt->variableNames.head != NULL && + !simple_string_list_member(&ropt->variableNames, te->tag)) + return 0; + } else return 0; } @@ -3816,6 +3824,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "TEXT SEARCH DICTIONARY") == 0 || strcmp(type, "TEXT SEARCH CONFIGURATION") == 0 || strcmp(type, "TYPE") == 0 || + strcmp(type, "VARIABLE") == 0 || strcmp(type, "VIEW") == 0 || /* non-schema-specified objects */ strcmp(type, "DATABASE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 9fc3671cb35..992e332c026 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -381,6 +381,7 @@ static void dumpPublication(Archive *fout, const PublicationInfo *pubinfo); static void dumpPublicationTable(Archive *fout, const PublicationRelInfo *pubrinfo); static void dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo); static void dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo); +static void dumpVariable(Archive *fout, const VariableInfo *varinfo); static void dumpDatabase(Archive *fout); static void dumpDatabaseConfig(Archive *AH, PQExpBuffer outbuf, const char *dbname, Oid dboid); @@ -5681,6 +5682,188 @@ get_next_possible_free_pg_type_oid(Archive *fout, PQExpBuffer upgrade_query) return next_possible_free_oid; } +/* + * getVariables + * get information about variables + */ +void +getVariables(Archive *fout) +{ + PQExpBuffer query; + PGresult *res; + VariableInfo *varinfo; + int i_tableoid; + int i_oid; + int i_varname; + int i_varnamespace; + int i_vartype; + int i_vartypname; + int i_varowner; + int i_varcollation; + int i_varacl; + int i_acldefault; + int i, + ntups; + + if (fout->remoteVersion < 180000) + return; + + query = createPQExpBuffer(); + + /* get the variables in current database */ + appendPQExpBuffer(query, + "SELECT v.tableoid, v.oid, v.varname,\n" + " v.varnamespace, v.vartype,\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as vartypname,\n" + " CASE WHEN v.varcollation <> t.typcollation " + " THEN v.varcollation\n" + " ELSE 0\n" + " END AS varcollation,\n" + " v.varowner, v.varacl,\n" + " acldefault('V', v.varowner) AS acldefault\n" + "FROM pg_catalog.pg_variable v\n" + "JOIN pg_catalog.pg_type t " + "ON (v.vartype = t.oid)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_tableoid = PQfnumber(res, "tableoid"); + i_oid = PQfnumber(res, "oid"); + i_varname = PQfnumber(res, "varname"); + i_varnamespace = PQfnumber(res, "varnamespace"); + i_vartype = PQfnumber(res, "vartype"); + i_vartypname = PQfnumber(res, "vartypname"); + i_varcollation = PQfnumber(res, "varcollation"); + + i_varowner = PQfnumber(res, "varowner"); + i_varacl = PQfnumber(res, "varacl"); + i_acldefault = PQfnumber(res, "acldefault"); + + varinfo = pg_malloc(ntups * sizeof(VariableInfo)); + + for (i = 0; i < ntups; i++) + { + TypeInfo *vtype; + + varinfo[i].dobj.objType = DO_VARIABLE; + varinfo[i].dobj.catId.tableoid = + atooid(PQgetvalue(res, i, i_tableoid)); + varinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&varinfo[i].dobj); + varinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_varname)); + varinfo[i].dobj.namespace = + findNamespace(atooid(PQgetvalue(res, i, i_varnamespace))); + + varinfo[i].vartype = atooid(PQgetvalue(res, i, i_vartype)); + varinfo[i].vartypname = pg_strdup(PQgetvalue(res, i, i_vartypname)); + varinfo[i].varcollation = atooid(PQgetvalue(res, i, i_varcollation)); + + varinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_varacl)); + varinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault)); + varinfo[i].dacl.privtype = 0; + varinfo[i].dacl.initprivs = NULL; + varinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_varowner)); + + /* do not try to dump ACL if no ACL exists */ + if (!PQgetisnull(res, i, i_varacl)) + varinfo[i].dobj.components |= DUMP_COMPONENT_ACL; + + if (strlen(varinfo[i].rolname) == 0) + pg_log_warning("owner of variable \"%s\" appears to be invalid", + varinfo[i].dobj.name); + + /* decide whether we want to dump it */ + selectDumpableObject(&(varinfo[i].dobj), fout); + + vtype = findTypeByOid(varinfo[i].vartype); + addObjectDependency(&varinfo[i].dobj, vtype->dobj.dumpId); + } + PQclear(res); + + destroyPQExpBuffer(query); +} + +/* + * dumpVariable + * dump the definition of the given session variable + */ +static void +dumpVariable(Archive *fout, const VariableInfo *varinfo) +{ + DumpOptions *dopt = fout->dopt; + + PQExpBuffer delq; + PQExpBuffer query; + char *qualvarname; + const char *vartypname; + Oid varcollation; + + /* skip if not to be dumped */ + if (!varinfo->dobj.dump || !dopt->dumpSchema) + return; + + delq = createPQExpBuffer(); + query = createPQExpBuffer(); + + qualvarname = pg_strdup(fmtQualifiedDumpable(varinfo)); + vartypname = varinfo->vartypname; + varcollation = varinfo->varcollation; + + appendPQExpBuffer(delq, "DROP VARIABLE %s;\n", + qualvarname); + + appendPQExpBuffer(query, "CREATE VARIABLE %s AS %s", + qualvarname, vartypname); + + if (OidIsValid(varcollation)) + { + CollInfo *coll; + + coll = findCollationByOid(varcollation); + if (coll) + appendPQExpBuffer(query, " COLLATE %s", + fmtQualifiedDumpable(coll)); + } + + appendPQExpBuffer(query, ";\n"); + + if (varinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) + ArchiveEntry(fout, varinfo->dobj.catId, varinfo->dobj.dumpId, + ARCHIVE_OPTS(.tag = varinfo->dobj.name, + .namespace = varinfo->dobj.namespace->dobj.name, + .owner = varinfo->rolname, + .description = "VARIABLE", + .section = SECTION_PRE_DATA, + .createStmt = query->data, + .dropStmt = delq->data)); + + /* dump comment if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, "VARIABLE", qualvarname, + NULL, varinfo->rolname, + varinfo->dobj.catId, 0, varinfo->dobj.dumpId); + + /* dump ACL if any */ + if (varinfo->dobj.dump & DUMP_COMPONENT_ACL) + { + char *qvarname = pg_strdup(fmtId(varinfo->dobj.name)); + + dumpACL(fout, varinfo->dobj.dumpId, InvalidDumpId, "VARIABLE", + qvarname, NULL, + varinfo->dobj.namespace->dobj.name, NULL, varinfo->rolname, + &varinfo->dacl); + + free(qvarname); + } + + destroyPQExpBuffer(delq); + destroyPQExpBuffer(query); + + free(qualvarname); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -11813,6 +11996,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) case DO_REL_STATS: dumpRelationStats(fout, (const RelStatsInfo *) dobj); break; + case DO_VARIABLE: + dumpVariable(fout, (VariableInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ @@ -16319,6 +16505,9 @@ dumpDefaultACL(Archive *fout, const DefaultACLInfo *daclinfo) case DEFACLOBJ_LARGEOBJECT: type = "LARGE OBJECTS"; break; + case DEFACLOBJ_VARIABLE: + type = "VARIABLES"; + break; default: /* shouldn't get here */ pg_fatal("unrecognized object type in default privileges: %d", @@ -20137,6 +20326,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_CONVERSION: case DO_TABLE: case DO_TABLE_ATTACH: + case DO_VARIABLE: case DO_ATTRDEF: case DO_PROCLANG: case DO_CAST: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index bcc94ff07cc..31e2b67deee 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -53,6 +53,7 @@ typedef enum DO_TABLE, DO_TABLE_ATTACH, DO_ATTRDEF, + DO_VARIABLE, DO_INDEX, DO_INDEX_ATTACH, DO_STATSEXT, @@ -745,6 +746,19 @@ typedef struct _SubRelInfo char *srsublsn; } SubRelInfo; +/* + * The VariableInfo struct is used to represent session variables + */ +typedef struct _VariableInfo +{ + DumpableObject dobj; + DumpableAcl dacl; + Oid vartype; + char *vartypname; + Oid varcollation; + const char *rolname; /* name of owner, or empty string */ +} VariableInfo; + /* * common utility functions */ @@ -829,5 +843,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); extern void getSubscriptionTables(Archive *fout); +extern void getVariables(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 2d02456664b..6b3fb9840ae 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -76,6 +76,7 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, + PRIO_VARIABLE, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, @@ -119,6 +120,7 @@ static const int dbObjectTypePriority[] = [DO_TABLE] = PRIO_TABLE, [DO_TABLE_ATTACH] = PRIO_TABLE_ATTACH, [DO_ATTRDEF] = PRIO_ATTRDEF, + [DO_VARIABLE] = PRIO_VARIABLE, [DO_INDEX] = PRIO_INDEX, [DO_INDEX_ATTACH] = PRIO_INDEX_ATTACH, [DO_STATSEXT] = PRIO_STATSEXT, @@ -1749,6 +1751,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "RELATION STATISTICS FOR %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_VARIABLE: + snprintf(buf, bufsize, + "VARIABLE %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; } /* shouldn't get here */ snprintf(buf, bufsize, diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index fc5b9b52f80..07f42e0cb31 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -1008,6 +1008,16 @@ my %tests = ( unlike => { no_privs => 1, }, }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC' + => { + create_order => 56, + create_sql => 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;', + regexp => qr/^ + \QALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT SELECT ON VARIABLES TO PUBLIC;\E/xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { no_privs => 1, }, + }, + 'ALTER ROLE regress_dump_test_role' => { regexp => qr/^ \QALTER ROLE regress_dump_test_role WITH \E @@ -2034,6 +2044,23 @@ my %tests = ( }, }, + 'COMMENT ON VARIABLE dump_test.variable1' => { + create_order => 71, + create_sql => 'COMMENT ON VARIABLE dump_test.variable1 + IS \'comment on variable\';', + regexp => + qr/^\QCOMMENT ON VARIABLE dump_test.variable1 IS 'comment on variable';\E/m, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'COPY test_table' => { create_order => 4, create_sql => 'INSERT INTO dump_test.test_table (col1) ' @@ -4405,6 +4432,23 @@ my %tests = ( }, }, + 'CREATE VARIABLE test_variable' => { + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS integer;\E/xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE VIEW test_view' => { create_order => 61, create_sql => 'CREATE VIEW dump_test.test_view @@ -4869,6 +4913,25 @@ my %tests = ( like => {}, }, + 'GRANT SELECT ON VARIABLE dump_test.variable1' => { + create_order => 73, + create_sql => + 'GRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT SELECT ON VARIABLE dump_test.variable1 TO regress_dump_test_role;\E + /xm, + like => { + %full_runs, + %dump_test_schema_runs, + section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'REFRESH MATERIALIZED VIEW matview' => { regexp => qr/^\QREFRESH MATERIALIZED VIEW dump_test.matview;\E/m, like => diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e35d9d7dfb0..9fb3909f7f6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3212,6 +3212,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt -- 2.51.0 [text/x-patch] v20250929-0004-support-of-session-variables-for-psql.patch (22.8K, 14-v20250929-0004-support-of-session-variables-for-psql.patch) download | inline diff: From 31f8f057d08aa0742d4bc8ddcb2a205815463c97 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_variable_is_visible</primary> + </indexterm> + <function>pg_variable_is_visible</function> ( <parameter>variable</parameter> <type>oid</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Is session variable visible in search path? + </para></entry> + </row> </tbody> </tgroup> </table> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry id="app-psql-meta-command-dv"> + <term><literal>\dV[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists session variables. + If <replaceable class="parameter">pattern</replaceable> is + specified, only session variables whose names match the pattern are listed. + If the form <literal>\dV+</literal> is used, additional information + about each variable is shown, like access privileges and description. + </para> + </listitem> + </varlistentry> + <varlistentry id="app-psql-meta-command-du"> <term><literal>\du[Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> <listitem> diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index df15852d906..338d78e174f 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5359,3 +5359,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 4aa793d7de7..1f5f7ced772 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6176741d20b..5fc5c15057b 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -988,6 +988,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1352,6 +1359,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1917,7 +1925,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2631,6 +2639,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE <name> */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW <name> */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3237,7 +3248,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4043,6 +4054,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE <name> with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE <name> with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW <name> with AS or WITH */ @@ -4320,6 +4338,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4521,7 +4545,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4529,6 +4555,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4543,7 +4570,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4551,7 +4579,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4587,6 +4616,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4913,7 +4944,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5402,6 +5433,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e5b48d3530d..d28fb3283cb 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6709,6 +6709,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.51.0 [text/x-patch] v20250929-0003-GRANT-REVOKE-variable.patch (54.4K, 15-v20250929-0003-GRANT-REVOKE-variable.patch) download | inline diff: From 72749fe1f970eb62f8d66938217962ce40d7a981 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Tue, 5 Aug 2025 06:37:28 +0200 Subject: [PATCH 03/15] GRANT, REVOKE variable Access to session variables can be controlled by SELECT or UPDATE rights. Both rights are introduced by this patch too. Default ACL are supported. --- doc/src/sgml/catalogs.sgml | 3 +- doc/src/sgml/ddl.sgml | 19 +- doc/src/sgml/func/func-info.sgml | 23 +- .../sgml/ref/alter_default_privileges.sgml | 29 +- doc/src/sgml/ref/grant.sgml | 20 +- doc/src/sgml/ref/revoke.sgml | 8 + src/backend/catalog/aclchk.c | 74 +++- src/backend/catalog/objectaddress.c | 22 +- src/backend/catalog/pg_variable.c | 11 +- src/backend/parser/gram.y | 21 +- src/backend/utils/adt/acl.c | 221 ++++++++++++ src/include/catalog/pg_default_acl.h | 1 + src/include/catalog/pg_proc.dat | 20 ++ src/include/parser/kwlist.h | 1 + src/include/utils/acl.h | 1 + .../expected/session_variables_acl.out | 335 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- .../regress/sql/session_variables_acl.sql | 182 ++++++++++ 18 files changed, 967 insertions(+), 26 deletions(-) create mode 100644 src/test/regress/expected/session_variables_acl.out create mode 100644 src/test/regress/sql/session_variables_acl.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 487a58eda74..af178013cf4 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3360,7 +3360,8 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l <literal>f</literal> = function, <literal>T</literal> = type, <literal>n</literal> = schema, - <literal>L</literal> = large object + <literal>L</literal> = large object, + <literal>V</literal> = session variable </para></entry> </row> diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc4..420a4d9ff11 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the <function>currval</function> function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. </para> </listitem> </varlistentry> @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <function>setval</function> functions. For large objects, this privilege allows writing or truncating the object. + For session variables, this privilege allows to set a value to the + object. </para> </listitem> </varlistentry> @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal> (and table-like objects), - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; <literal>LARGE OBJECT</literal>, <literal>SEQUENCE</literal>, <literal>TABLE</literal>, - table column + table column, + <literal>SESSION VARIABLE</literal> </entry> </row> <row> @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; <entry><literal>U</literal></entry> <entry><literal>\dT+</literal></entry> </row> + <row> + <entry><literal>SESSION VARIABLE</literal></entry> + <entry><literal>rw</literal></entry> + <entry><literal>none</literal></entry> + <entry><literal>\dV+</literal></entry> + </row> </tbody> </tgroup> </table> @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; <para> Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via <command>GRANT</command> and <command>REVOKE</command> + commands. A session variable can be created by the <command>CREATE + VARIABLE</command> command. </para> <para> diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..a57f7665054 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -740,6 +740,25 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>has_session_variable_privilege</primary> + </indexterm> + <function>has_session_variable_privilege</function> ( + <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional> + <parameter>session_variable</parameter> <type>text</type> or <type>oid</type>, + <parameter>privilege</parameter> <type>text</type> ) + <returnvalue>boolean</returnvalue> + </para> + <para> + Does user have privilege for session variable? + Allowable privilege types are + <literal>SELECT</literal>, and + <literal>UPDATE</literal>. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for <literal>TABLESPACE</literal>, 'F' for <literal>FOREIGN DATA WRAPPER</literal>, 'S' for <literal>FOREIGN SERVER</literal>, - or - 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal>. + 'T' for <literal>TYPE</literal> or <literal>DOMAIN</literal> or + 'V' for <literal>SESSION VARIABLE</literal>. </para></entry> </row> diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df91..bc73817061f 100644 --- a/doc/src/sgml/ref/alter_default_privileges.sgml +++ b/doc/src/sgml/ref/alter_default_privileges.sgml @@ -56,6 +56,11 @@ GRANT { { SELECT | UPDATE } ON LARGE OBJECTS TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ WITH GRANT OPTION ] + REVOKE [ GRANT OPTION FOR ] { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER | MAINTAIN } [, ...] | ALL [ PRIVILEGES ] } @@ -95,6 +100,14 @@ REVOKE [ GRANT OPTION FOR ] ON LARGE OBJECTS FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] <replaceable class="parameter">role_name</replaceable> | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] </synopsis> </refsynopsisdiv> @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] <para> Currently, only the privileges for schemas, tables (including views and foreign - tables), sequences, functions, types (including domains), and large objects - can be altered. For this command, functions include aggregates and procedures. - The words <literal>FUNCTIONS</literal> and <literal>ROUTINES</literal> are - equivalent in this command. (<literal>ROUTINES</literal> is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word <literal>FUNCTIONS</literal> was allowed. It is not possible to set - default privileges for functions and procedures separately.) + tables), sequences, functions, types (including domains), large objects + and session variables can be altered. For this command, functions include + aggregates and procedures. The words <literal>FUNCTIONS</literal> and + <literal>ROUTINES</literal> are equivalent in this command. + (<literal>ROUTINES</literal> is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word <literal>FUNCTIONS</literal> was allowed. It is not possible + to set default privileges for functions and procedures separately.) </para> <para> diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c0..c11860fa200 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace [ WITH { ADMIN | INHERIT | SET } { OPTION | TRUE | FALSE } ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> @@ -119,8 +125,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace that grants privileges on a database object (table, column, view, foreign table, sequence, database, foreign-data wrapper, foreign server, function, procedure, procedural language, large object, configuration - parameter, schema, tablespace, or type), and one that grants - membership in a role. These variants are similar in many ways, but + parameter, schema, session variable, tablespace, or type), and one that + grants membership in a role. These variants are similar in many ways, but they are different enough to be described separately. </para> @@ -236,9 +242,9 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace <para> There is also an option to grant privileges on all objects of the same type within one or more schemas. This functionality is currently supported - only for tables, sequences, functions, and procedures. <literal>ALL - TABLES</literal> also affects views and foreign tables, just like the - specific-object <command>GRANT</command> command. <literal>ALL + only for tables, sequences, functions, procedures and variables. + <literal>ALL TABLES</literal> also affects views and foreign tables, just + like the specific-object <command>GRANT</command> command. <literal>ALL FUNCTIONS</literal> also affects aggregate and window functions, but not procedures, again just like the specific-object <command>GRANT</command> command. Use <literal>ALL ROUTINES</literal> to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; </para> <para> - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are <productname>PostgreSQL</productname> extensions. </para> </refsect1> diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1..760fddb7c20 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -130,6 +130,14 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE <replaceable>variable_name</replaceable> [, ...] + | ALL VARIABLES IN SCHEMA <replaceable class="parameter">schema_name</replaceable> [, ...] } + FROM { <replaceable class="parameter">role_specification</replaceable> | PUBLIC } [, ...] + [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ] + [ CASCADE | RESTRICT ] + <phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase> [ GROUP ] <replaceable class="parameter">role_name</replaceable> diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 00e3630e0ec..93c10beebe9 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -64,6 +64,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -290,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -534,6 +538,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -639,6 +647,9 @@ ExecGrantStmt_oids(InternalGrant *istmt) case OBJECT_PARAMETER_ACL: ExecGrant_Parameter(istmt); break; + case OBJECT_VARIABLE: + ExecGrant_common(istmt, VariableRelationId, ACL_ALL_RIGHTS_VARIABLE, NULL); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) istmt->objtype); @@ -773,6 +784,18 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) objects = lappend_oid(objects, parameterId); } break; + + case OBJECT_VARIABLE: + foreach_node(RangeVar, varvar, objnames) + { + Oid relOid; + + relOid = LookupVariable(varvar->schemaname, + varvar->relname, + false); + objects = lappend_oid(objects, relOid); + } + break; } return objects; @@ -859,6 +882,32 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) table_close(rel, AccessShareLock); } break; + case OBJECT_VARIABLE: + { + ScanKeyData key; + Relation rel; + TableScanDesc scan; + HeapTuple tuple; + + ScanKeyInit(&key, + Anum_pg_variable_varnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceId)); + + rel = table_open(VariableRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 1, &key); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Oid oid = ((Form_pg_proc) GETSTRUCT(tuple))->oid; + + objects = lappend_oid(objects, oid); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + } + break; default: /* should not happen */ elog(ERROR, "unrecognized GrantStmt.objtype: %d", @@ -1022,6 +1071,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for session variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1222,6 +1275,11 @@ SetDefaultACL(InternalDefaultACL *iacls) if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", @@ -1469,6 +1527,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_LARGEOBJECT: iacls.objtype = OBJECT_LARGEOBJECT; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1529,6 +1590,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case ParameterAclRelationId: istmt.objtype = OBJECT_PARAMETER_ACL; break; + case VariableRelationId: + istmt.objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unexpected object class %u", classid); break; @@ -2762,6 +2826,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for session variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -2784,7 +2851,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: - case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -3025,6 +3091,8 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, return ACL_NO_RIGHTS; case OBJECT_TYPE: return object_aclmask(TypeRelationId, object_oid, roleid, mask, how); + case OBJECT_VARIABLE: + return object_aclmask(VariableRelationId, object_oid, roleid, mask, how); default: elog(ERROR, "unrecognized object type: %d", (int) objtype); @@ -4288,6 +4356,10 @@ get_user_default_acl(ObjectType objtype, Oid ownerId, Oid nsp_oid) defaclobjtype = DEFACLOBJ_LARGEOBJECT; break; + case OBJECT_VARIABLE: + defaclobjtype = DEFACLOBJ_VARIABLE; + break; + default: return NULL; } diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index ac1f8a4db3b..32cd078c162 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -2029,17 +2029,21 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_LARGEOBJECT: objtype_str = "large objects"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; + break; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized default ACL object type \"%c\"", objtype), - errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", + errhint("Valid object types are \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\", \"%c\".", DEFACLOBJ_RELATION, DEFACLOBJ_SEQUENCE, DEFACLOBJ_FUNCTION, DEFACLOBJ_TYPE, DEFACLOBJ_NAMESPACE, - DEFACLOBJ_LARGEOBJECT))); + DEFACLOBJ_LARGEOBJECT, + DEFACLOBJ_VARIABLE))); } /* @@ -3921,6 +3925,16 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) _("default privileges on new large objects belonging to role %s"), rolename); break; + case DEFACLOBJ_VARIABLE: + if (nspname) + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s in schema %s"), + rolename, nspname); + else + appendStringInfo(&buffer, + _("default privileges on new session variables belonging to role %s"), + rolename); + break; default: /* shouldn't get here */ if (nspname) @@ -5851,6 +5865,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on large objects"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on session variables"); + break; } if (objname) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index bd6a29a79e5..d8ede4fa8c8 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -38,6 +38,7 @@ create_variable(const char *varName, Oid varCollation, bool if_not_exists) { + Acl *varacl; NameData varname; bool nulls[Natts_pg_variable]; Datum values[Natts_pg_variable]; @@ -97,7 +98,12 @@ create_variable(const char *varName, values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); - nulls[Anum_pg_variable_varacl - 1] = true; + varacl = get_user_default_acl(OBJECT_VARIABLE, varOwner, + varNamespace); + if (varacl != NULL) + values[Anum_pg_variable_varacl - 1] = PointerGetDatum(varacl); + else + nulls[Anum_pg_variable_varacl - 1] = true; tupdesc = RelationGetDescr(rel); @@ -131,6 +137,9 @@ create_variable(const char *varName, /* dependency on owner */ recordDependencyOnOwner(VariableRelationId, varid, varOwner); + /* dependencies on roles mentioned in default ACL */ + recordDependencyOnNewAcl(VariableRelationId, varid, 0, varOwner, varacl); + /* dependency on extension */ recordDependencyOnCurrentExtension(&myself, false); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 51b8778db5c..6926cea75b9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -784,7 +784,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE - VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VARIABLES VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8059,6 +8059,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | ALL TABLES IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -8104,6 +8112,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } ; @@ -8302,6 +8318,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18122,6 +18139,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18779,6 +18797,7 @@ bare_label_keyword: | VALUES | VARCHAR | VARIABLE + | VARIABLES | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index fbcd64a2609..9828382b900 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -31,6 +31,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "common/hashfn.h" @@ -128,6 +129,8 @@ static AclMode convert_type_priv_string(text *priv_type_text); static AclMode convert_parameter_priv_string(text *priv_text); static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); +static Oid convert_session_variable_name(text *varname); +static AclMode convert_session_variable_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); @@ -867,6 +870,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -964,6 +971,9 @@ acldefault_sql(PG_FUNCTION_ARGS) case 'T': objtype = OBJECT_TYPE; break; + case 'V': + objtype = OBJECT_VARIABLE; + break; default: elog(ERROR, "unrecognized object type abbreviation: %c", objtypec); } @@ -5032,6 +5042,217 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) return ACLCHECK_NO_PRIV; } +/* + * has_session_variable_privilege variants + * These are all named "has_session_variable_privilege" at the SQL level. + * They take various combinations of variable name, variable OID, + * user name, user OID, or implicit user = current_user. + * + * The result is a boolean value: true if user has the indicated + * privilege, false if not, or NULL if session variable doesn't + * exists. + */ + +/* + * has_session_variable_privilege_name_name + * Check user privileges on a session variable given + * name username, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_name_name(PG_FUNCTION_ARGS) +{ + Name rolename = PG_GETARG_NAME(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*rolename)); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name + * Check user privileges on a session variable given + * text session variable and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_name(PG_FUNCTION_ARGS) +{ + text *varname = PG_GETARG_TEXT_PP(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_name_id + * Check user privileges on a session variable given + * name usename, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_name_id(PG_FUNCTION_ARGS) +{ + Name username = PG_GETARG_NAME(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = get_role_oid_or_public(NameStr(*username)); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id + * Check user privileges on a session variable given + * session variable oid, and text priv name. + * current_user is assumed + */ +Datum +has_session_variable_privilege_id(PG_FUNCTION_ARGS) +{ + Oid varid = PG_GETARG_OID(0); + text *priv_type_text = PG_GETARG_TEXT_PP(1); + Oid roleid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + roleid = GetUserId(); + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_name + * Check user privileges on a session variable given + * roleid, text session variable name, and text priv name. + */ +Datum +has_session_variable_privilege_id_name(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + text *varname = PG_GETARG_TEXT_PP(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + Oid varid; + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + varid = convert_session_variable_name(varname); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * has_session_variable_privilege_id_id + * Check user privileges on a session variable given + * roleid, session variable oid, and text priv name. + */ +Datum +has_session_variable_privilege_id_id(PG_FUNCTION_ARGS) +{ + Oid roleid = PG_GETARG_OID(0); + Oid varid = PG_GETARG_OID(1); + text *priv_type_text = PG_GETARG_TEXT_PP(2); + AclMode mode; + AclResult aclresult; + bool is_missing = false; + + mode = convert_session_variable_priv_string(priv_type_text); + + aclresult = object_aclcheck_ext(VariableRelationId, varid, + roleid, mode, + &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + + PG_RETURN_BOOL(aclresult == ACLCHECK_OK); +} + +/* + * Given a session variable name expressed as a string, look it up and return + * Oid + */ +static Oid +convert_session_variable_name(text *varname) +{ + return LookupVariableFromNameList(textToQualifiedNameList(varname), true); +} + +/* + * convert_variable_priv_string + * Convert text string to AclMode value. + */ +static AclMode +convert_session_variable_priv_string(text *priv_type_text) +{ + static const priv_map session_variable_priv_map[] = { + {"SELECT", ACL_SELECT}, + {"SELECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_SELECT)}, + {"UPDATE", ACL_UPDATE}, + {"UPDATE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_UPDATE)}, + {NULL, 0} + }; + + return convert_any_priv_string(priv_type_text, session_variable_priv_map); +} /* * initialization function (called by InitPostgres) diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf..087d35b943d 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -69,6 +69,7 @@ MAKE_SYSCACHE(DEFACLROLENSPOBJ, pg_default_acl_role_nsp_obj_index, 8); #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ #define DEFACLOBJ_LARGEOBJECT 'L' /* large object */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 01eba3b5a19..e5b48d3530d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5479,6 +5479,26 @@ prorettype => 'bool', proargtypes => 'oid oid text', prosrc => 'has_largeobject_privilege_id_id' }, +{ oid => '9613', descr => 'user privilege on session variable by username, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name text text', + prosrc => 'has_session_variable_privilege_name_name' }, +{ oid => '9614', descr => 'user privilege on session variable by username, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'name oid text', prosrc => 'has_session_variable_privilege_name_id' }, +{ oid => '9615', descr => 'user privilege on session variable by user oid, seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text text', prosrc => 'has_session_variable_privilege_id_name' }, +{ oid => '9616', descr => 'user privilege on session variable by user oid, seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid oid text', prosrc => 'has_session_variable_privilege_id_id' }, +{ oid => '9617', descr => 'current user privilege on session variable by seq name', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'text text', prosrc => 'has_session_variable_privilege_name' }, +{ oid => '9618', descr => 'current user privilege on session variable by seq oid', + proname => 'has_session_variable_privilege', provolatile => 's', prorettype => 'bool', + proargtypes => 'oid text', prosrc => 'has_session_variable_privilege_id' }, + { oid => '3355', descr => 'I/O', proname => 'pg_ndistinct_in', prorettype => 'pg_ndistinct', proargtypes => 'cstring', prosrc => 'pg_ndistinct_in' }, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 6f513f04225..0ea0265de7c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -487,6 +487,7 @@ PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd..5e1a8a82e90 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -169,6 +169,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/session_variables_acl.out b/src/test/regress/expected/session_variables_acl.out new file mode 100644 index 00000000000..f2219529916 --- /dev/null +++ b/src/test/regress/expected/session_variables_acl.out @@ -0,0 +1,335 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT UPDATE ON VARIABLES TO regress_variable_reader_acl; +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT INSERT ON VARIABLES TO regress_variable_reader_acl; +ERROR: invalid privilege type INSERT for session variable +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT DELETE ON VARIABLES TO regress_variable_reader_acl; +ERROR: invalid privilege type DELETE for session variable +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO regress_variable_owner_acl; +-- should to fail +GRANT INSERT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +ERROR: invalid privilege type INSERT for session variable +GRANT DELETE ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +ERROR: invalid privilege type DELETE for session variable +-- should be ok +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE public.sesvar22_acl AS int; +SET search_path TO public; +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + ?column? +---------- + t +(1 row) + +SET ROLE TO regress_variable_r1_acl; +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f + has_session_variable_privilege +-------------------------------- + f +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t + has_session_variable_privilege +-------------------------------- + t +(1 row) + +-- +-- end of function has_session_variable_privilege tests. +-- +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; +DROP VARIABLE public.sesvar22_acl; +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3abc1aca5f2..95c76baf9ae 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl session_variables_acl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/session_variables_acl.sql b/src/test/regress/sql/session_variables_acl.sql new file mode 100644 index 00000000000..4b3653cd4ed --- /dev/null +++ b/src/test/regress/sql/session_variables_acl.sql @@ -0,0 +1,182 @@ +-- check access rights and supported ALTER +CREATE SCHEMA svartest_acl; +CREATE ROLE regress_variable_owner_acl; +CREATE ROLE regress_variable_reader_acl; + +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +GRANT ALL ON SCHEMA public TO regress_variable_owner_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT SELECT ON VARIABLES TO regress_variable_reader_acl; + +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT UPDATE ON VARIABLES TO regress_variable_reader_acl; + +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT INSERT ON VARIABLES TO regress_variable_reader_acl; + +-- should to fail +ALTER DEFAULT PRIVILEGES + FOR ROLE regress_variable_owner_acl + IN SCHEMA svartest_acl + GRANT DELETE ON VARIABLES TO regress_variable_reader_acl; + +-- creating variable with default privileges +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE svartest_acl.sesvar20 AS int; +SET ROLE TO DEFAULT; + +-- should be ok. since ALTER DEFAULT PRIVILEGES +-- allow regress_variable_reader_acl to have SELECT priviledge +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_reader_acl', 'svartest_acl.sesvar20', 'UPDATE'); -- t + +DROP VARIABLE svartest_acl.sesvar20; +DROP SCHEMA svartest_acl; +DROP ROLE regress_variable_reader_acl; + +-- +-- begin of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- +CREATE ROLE regress_variable_r1_acl; +CREATE ROLE regress_variable_r2_acl; + +SET ROLE TO regress_variable_owner_acl; +CREATE VARIABLE sesvar22_acl AS int; --sesvar22_acl will owned by regress_variable_owner_acl + +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r1_acl WITH GRANT OPTION; +SET ROLE TO regress_variable_r1_acl; +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl WITH GRANT OPTION; +SET ROLE TO DEFAULT; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL PRIVILEGES ON VARIABLE sesvar22_acl FROM regress_variable_r1_acl CASCADE; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f + +SET ROLE TO regress_variable_owner_acl; + +-- should to fail +GRANT INSERT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; +GRANT DELETE ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +-- should be ok +GRANT SELECT ON VARIABLE sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- t + +REVOKE ALL ON VARIABLE sesvar22_acl FROM regress_variable_r2_acl GRANTED BY regress_variable_owner_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'public.sesvar22_acl', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_owner_acl', 'public.sesvar22_acl', 'SELECT'); -- t +SET ROLE TO DEFAULT; + +DROP VARIABLE sesvar22_acl; +-- +-- end of check GRANT WITH GRANT OPTION and REVOKE GRANTED BY +-- + +-- +-- begin of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- +CREATE SCHEMA svartest_acl; +GRANT ALL ON SCHEMA svartest_acl TO regress_variable_owner_acl; +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE svartest_acl.sesvar20 AS int; +CREATE VARIABLE svartest_acl.sesvar21 AS int; + +GRANT SELECT ON ALL VARIABLES IN SCHEMA svartest_acl TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- t + +REVOKE SELECT ON ALL VARIABLES IN SCHEMA svartest_acl FROM regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar20', 'SELECT'); -- f +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'svartest_acl.sesvar21', 'SELECT'); -- f + +SET ROLE TO DEFAULT; +DROP VARIABLE svartest_acl.sesvar20; +DROP VARIABLE svartest_acl.sesvar21; +DROP SCHEMA svartest_acl; +-- +-- end of test: GRANT|REVOKE SELECT|UPDATE ON ALL VARIABLES IN SCHEMA +-- + +-- +-- function has_session_variable_privilege have various kind of signature. +-- the following are extensive test for it. +-- +SET ROLE TO regress_variable_owner_acl; + +CREATE VARIABLE public.sesvar22_acl AS int; + +SET search_path TO public; + +GRANT SELECT ON VARIABLE public.sesvar22_acl TO regress_variable_r1_acl; +GRANT SELECT, UPDATE ON VARIABLE public.sesvar22_acl TO regress_variable_r2_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.sesvar22_acl', 'SELECT'); +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'public.notexists', 'SELECT') IS NULL; + +SET ROLE TO regress_variable_r1_acl; + +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT oid AS varid + FROM pg_variable + WHERE varname = 'sesvar22_acl' AND varnamespace = 'public'::regnamespace \gset + +SELECT has_session_variable_privilege('sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('sesvar22_acl', 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl', :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl', :varid, 'UPDATE'); -- t + +SELECT has_session_variable_privilege(:varid, 'SELECT'); -- t +SELECT has_session_variable_privilege(:varid, 'UPDATE'); -- f + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, 'sesvar22_acl', 'UPDATE'); -- t + +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r1_acl'::regrole, :varid, 'UPDATE'); -- f +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'SELECT'); -- t +SELECT has_session_variable_privilege('regress_variable_r2_acl'::regrole, :varid, 'UPDATE'); -- t +-- +-- end of function has_session_variable_privilege tests. +-- + +SET ROLE TO DEFAULT; +SET search_path TO DEFAULT; + +DROP VARIABLE public.sesvar22_acl; + +DROP ROLE regress_variable_r1_acl; +DROP ROLE regress_variable_r2_acl; + +REVOKE ALL ON SCHEMA public FROM regress_variable_owner_acl; +DROP ROLE regress_variable_owner_acl; -- 2.51.0 [text/x-patch] v20250929-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20250929-0002-CREATE-DROP-ALTER-VARIABLE.patch) download | inline diff: From 45abe1158b2c99600e7390e963e966ef156ee008 Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 11:26:17 +0200 Subject: [PATCH 02/15] CREATE, DROP, ALTER VARIABLE Implementation of commands: CREATE VARIABLE varname AS type DROP VARIABLE varname ALTER VARIABLE varname OWNER TO ALTER VARIABLE varname RENAME TO ALTER VARIABLE varname SET SCHEMA ALTER command uses already prepared infrastructure based on ObjectAddress API, so this patch implements ObjectAddress related functionality too. Event triggers for DDL over session variables are supported. --- doc/src/sgml/ddl.sgml | 21 ++ doc/src/sgml/glossary.sgml | 15 ++ doc/src/sgml/plpgsql.sgml | 14 ++ doc/src/sgml/ref/allfiles.sgml | 3 + doc/src/sgml/ref/alter_variable.sgml | 178 +++++++++++++++ doc/src/sgml/ref/comment.sgml | 1 + doc/src/sgml/ref/create_schema.sgml | 12 +- doc/src/sgml/ref/create_variable.sgml | 149 +++++++++++++ doc/src/sgml/ref/drop_variable.sgml | 117 ++++++++++ doc/src/sgml/reference.sgml | 3 + src/backend/catalog/aclchk.c | 4 + src/backend/catalog/dependency.c | 6 + src/backend/catalog/namespace.c | 207 ++++++++++++++++++ src/backend/catalog/objectaddress.c | 99 +++++++++ src/backend/catalog/pg_shdepend.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/alter.c | 9 + src/backend/commands/dropcmds.c | 4 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/meson.build | 1 + src/backend/commands/seclabel.c | 1 + src/backend/commands/session_variable.c | 88 ++++++++ src/backend/commands/tablecmds.c | 41 ++++ src/backend/commands/typecmds.c | 15 ++ src/backend/parser/gram.y | 86 +++++++- src/backend/parser/parse_utilcmd.c | 12 + src/backend/tcop/utility.c | 20 ++ src/backend/utils/cache/lsyscache.c | 65 ++++++ src/include/catalog/namespace.h | 6 + src/include/commands/session_variable.h | 24 ++ src/include/nodes/parsenodes.h | 16 ++ src/include/parser/kwlist.h | 1 + src/include/tcop/cmdtaglist.h | 3 + src/include/utils/lsyscache.h | 4 + src/test/regress/expected/dependency.out | 17 ++ .../expected/session_variables_ddl.out | 163 ++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/dependency.sql | 14 ++ .../regress/sql/session_variables_ddl.sql | 150 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 40 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 doc/src/sgml/ref/alter_variable.sgml create mode 100644 doc/src/sgml/ref/create_variable.sgml create mode 100644 doc/src/sgml/ref/drop_variable.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/test/regress/expected/session_variables_ddl.out create mode 100644 src/test/regress/sql/session_variables_ddl.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 65bc070d2e5..fa711a09bc4 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; </para> </sect1> + <sect1 id="ddl-session-variables"> + <title>Session Variables</title> + + <indexterm zone="ddl-session-variables"> + <primary>Session variables</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + </indexterm> + + <para> + Session variables are database objects that can hold a value. + </para> + + <para> + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + </para> + </sect1> + <sect1 id="ddl-others"> <title>Other Database Objects</title> diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8651f0cdb91..c37fd5da50b 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1711,6 +1711,21 @@ </glossdef> </glossentry> + <glossentry id="glossary-session-variable"> + <glossterm>Session variable</glossterm> + <glossdef> + <para> + A persistent database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + Read or write access to session variables is controlled by privileges, + similar to other database objects. + </para> + <para> + For more information, see <xref linkend="ddl-session-variables"/>. + </para> + </glossdef> + </glossentry> + <glossentry id="glossary-shared-memory"> <glossterm>Shared memory</glossterm> <glossdef> diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; </programlisting> </para> </sect3> + + <sect3 id="plpgsql-porting-package-variables"> + <title><command>Packages and package variables</command></title> + + <para> + The <application>PL/pgSQL</application> language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + <productname>PostgreSQL</productname>. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see <xref linkend="ddl-session-variables"/>). + </para> + </sect3> </sect2> <sect2 id="plpgsql-porting-appendix"> diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867a..2f67de3e21b 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -47,6 +47,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY alterType SYSTEM "alter_type.sgml"> <!ENTITY alterUser SYSTEM "alter_user.sgml"> <!ENTITY alterUserMapping SYSTEM "alter_user_mapping.sgml"> +<!ENTITY alterVariable SYSTEM "alter_variable.sgml"> <!ENTITY alterView SYSTEM "alter_view.sgml"> <!ENTITY analyze SYSTEM "analyze.sgml"> <!ENTITY begin SYSTEM "begin.sgml"> @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY createType SYSTEM "create_type.sgml"> <!ENTITY createUser SYSTEM "create_user.sgml"> <!ENTITY createUserMapping SYSTEM "create_user_mapping.sgml"> +<!ENTITY createVariable SYSTEM "create_variable.sgml"> <!ENTITY createView SYSTEM "create_view.sgml"> <!ENTITY deallocate SYSTEM "deallocate.sgml"> <!ENTITY declare SYSTEM "declare.sgml"> @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. <!ENTITY dropType SYSTEM "drop_type.sgml"> <!ENTITY dropUser SYSTEM "drop_user.sgml"> <!ENTITY dropUserMapping SYSTEM "drop_user_mapping.sgml"> +<!ENTITY dropVariable SYSTEM "drop_variable.sgml"> <!ENTITY dropView SYSTEM "drop_view.sgml"> <!ENTITY end SYSTEM "end.sgml"> <!ENTITY execute SYSTEM "execute.sgml"> diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 00000000000..96d2586423e --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ +<!-- +doc/src/sgml/ref/alter_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-altervariable"> + <indexterm zone="sql-altervariable"> + <primary>ALTER VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>altering</secondary> + </indexterm> + + <refmeta> + <refentrytitle>ALTER VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>ALTER VARIABLE</refname> + <refpurpose> + change the definition of a session variable + </refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable> +ALTER VARIABLE <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable> +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + The <command>ALTER VARIABLE</command> command changes the definition of an + existing session variable. There are several subforms: + + <variablelist> + <varlistentry> + <term><literal>OWNER</literal></term> + <listitem> + <para> + This form changes the owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RENAME</literal></term> + <listitem> + <para> + This form changes the name of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>SET SCHEMA</literal></term> + <listitem> + <para> + This form moves the session variable into another schema. + </para> + </listitem> + </varlistentry> + + </variablelist> + </para> + + <para> + Only the owner or a superuser is allowed to alter a session variable. + In order to move a session variable from one schema to another, the user + must also have the <literal>CREATE</literal> privilege on the new schema (or + be a superuser). + + In order to move the session variable ownership from one role to another, + the user must also be a direct or indirect member of the new + owning role, and that role must have the <literal>CREATE</literal> privilege + on the session variable's schema (or be a superuser). These restrictions + enforce that altering the owner doesn't do anything you couldn't do by + dropping and recreating the session variable. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <para> + <variablelist> + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name (possibly schema-qualified) of the existing session variable + to alter. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_owner</replaceable></term> + <listitem> + <para> + The user name of the new owner of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_name</replaceable></term> + <listitem> + <para> + The new name for the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">new_schema</replaceable></term> + <listitem> + <para> + The new schema for the session variable. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To rename a session variable: +<programlisting> +ALTER VARIABLE foo RENAME TO boo; +</programlisting> + </para> + + <para> + To change the owner of the session variable <literal>boo</literal> to + <literal>joe</literal>: +<programlisting> +ALTER VARIABLE boo OWNER TO joe; +</programlisting> + </para> + + <para> + To change the schema of the session variable <literal>boo</literal> to + <literal>private</literal>: +<programlisting> +ALTER VARIABLE boo SET SCHEMA private; +</programlisting> + </para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + Session variables and this command in particular are a PostgreSQL extension. + </para> + </refsect1> + + <refsect1 id="sql-altervariable-see-also"> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-createvariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> +</refentry> diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b133..21cd80818fb 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR <replaceable>type_name</replaceable> LANGUAGE <replaceable>lang_name</replaceable> | TRIGGER <replaceable class="parameter">trigger_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> | TYPE <replaceable class="parameter">object_name</replaceable> | + VARIABLE <replaceable class="parameter">object_name</replaceable> | VIEW <replaceable class="parameter">object_name</replaceable> } IS { <replaceable class="parameter">string_literal</replaceable> | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6..d2bb265209b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -103,9 +103,10 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION <replaceable class="parameter">role_sp schema. Currently, only <command>CREATE TABLE</command>, <command>CREATE VIEW</command>, <command>CREATE INDEX</command>, <command>CREATE SEQUENCE</command>, <command>CREATE - TRIGGER</command> and <command>GRANT</command> are accepted as clauses - within <command>CREATE SCHEMA</command>. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER</command>, <command>GRANT</command> and <command>CREATE + VARIABLE</command> are accepted as clauses within <command>CREATE + SCHEMA</command>. Other kinds of objects may be created in separate + commands after the schema is created. </para> </listitem> </varlistentry> @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The <literal>IF NOT EXISTS</literal> option is a <productname>PostgreSQL</productname> extension. </para> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> </refsect1> <refsect1> diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 00000000000..6e988f2e472 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ +<!-- +doc/src/sgml/ref/create_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-createvariable"> + <indexterm zone="sql-createvariable"> + <primary>CREATE VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>defining</secondary> + </indexterm> + + <refmeta> + <refentrytitle>CREATE VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>CREATE VARIABLE</refname> + <refpurpose>define a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +CREATE VARIABLE [ IF NOT EXISTS ] <replaceable class="parameter">name</replaceable> [ AS ] <replaceable class="parameter">data_type</replaceable> [ COLLATE <replaceable class="parameter">collation</replaceable> ] +</synopsis> + </refsynopsisdiv> + <refsect1> + <title>Description</title> + + <para> + The <command>CREATE VARIABLE</command> command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands <command>GRANT</command> and <command>REVOKE</command>. + </para> + + <para> + The value of a session variable is local to the current session. Retrieving + a session variable's value returns NULL, unless its value is set to + something else in the current session with a <command>LET</command> command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + </para> + + <para> + Session variables are retrieved by the <command>SELECT</command> + command. Their value is set with the <command>LET</command> command. + </para> + + <note> + <para> + Session variables can be <quote>shadowed</quote> by other identifiers. + For details, see <xref linkend="ddl-session-variables"/>. + </para> + </note> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + + <varlistentry id="sql-createvariable-if-not-exists"> + <term><literal>IF NOT EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the name already exists. A notice is issued in + this case. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-name"> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-data_type"> + <term><replaceable class="parameter">data_type</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of the data type of the session + variable. + </para> + </listitem> + </varlistentry> + + <varlistentry id="sql-createvariable-collate"> + <term><literal>COLLATE <replaceable>collation</replaceable></literal></term> + <listitem> + <para> + The <literal>COLLATE</literal> clause assigns a collation to the session + variable (which must be of a collatable data type). If not specified, + the data type's default collation is used. + </para> + </listitem> + </varlistentry> + + </variablelist> + </refsect1> + + <refsect1> + <title>Notes</title> + + <para> + Use the <command>DROP VARIABLE</command> command to remove a session + variable. + </para> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + Create an date session variable <literal>var1</literal>: +<programlisting> +CREATE VARIABLE var1 AS date; +</programlisting> + </para> + + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>CREATE VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-dropvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 00000000000..5bdb3560f0b --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ +<!-- +doc/src/sgml/ref/drop_variable.sgml +PostgreSQL documentation +--> + +<refentry id="sql-dropvariable"> + <indexterm zone="sql-dropvariable"> + <primary>DROP VARIABLE</primary> + </indexterm> + + <indexterm> + <primary>session variable</primary> + <secondary>removing</secondary> + </indexterm> + + <refmeta> + <refentrytitle>DROP VARIABLE</refentrytitle> + <manvolnum>7</manvolnum> + <refmiscinfo>SQL - Language Statements</refmiscinfo> + </refmeta> + + <refnamediv> + <refname>DROP VARIABLE</refname> + <refpurpose>remove a session variable</refpurpose> + </refnamediv> + + <refsynopsisdiv> +<synopsis> +DROP VARIABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ] +</synopsis> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para> + <command>DROP VARIABLE</command> removes a session variable. + A session variable can only be removed by its owner or a superuser. + </para> + </refsect1> + + <refsect1> + <title>Parameters</title> + + <variablelist> + <varlistentry> + <term><literal>IF EXISTS</literal></term> + <listitem> + <para> + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><replaceable class="parameter">name</replaceable></term> + <listitem> + <para> + The name, optionally schema-qualified, of a session variable. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>CASCADE</literal></term> + <listitem> + <para> + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see <xref linkend="ddl-depend"/>). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>RESTRICT</literal></term> + <listitem> + <para> + Refuse to drop the session variable if any objects depend on it. This is + the default. + </para> + </listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>Examples</title> + + <para> + To remove the session variable <literal>var1</literal>: + +<programlisting> +DROP VARIABLE var1; +</programlisting></para> + </refsect1> + + <refsect1> + <title>Compatibility</title> + + <para> + The <command>DROP VARIABLE</command> command is a + <productname>PostgreSQL</productname> extension. + </para> + </refsect1> + + <refsect1> + <title>See Also</title> + + <simplelist type="inline"> + <member><xref linkend="sql-altervariable"/></member> + <member><xref linkend="sql-createvariable"/></member> + </simplelist> + </refsect1> + +</refentry> diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc..25578f3946c 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -75,6 +75,7 @@ &alterType; &alterUser; &alterUserMapping; + &alterVariable; &alterView; &analyze; &begin; @@ -127,6 +128,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +177,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index cd139bd65a6..00e3630e0ec 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -2784,6 +2784,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: elog(ERROR, "unsupported object type: %d", objtype); } @@ -2891,6 +2892,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TSDICTIONARY: msg = gettext_noop("must be owner of text search dictionary %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of session variable %s"); + break; /* * Special cases: For these, the error message talks diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7dded634eb8..1d62e63d4f7 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -65,12 +65,14 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" #include "commands/policy.h" #include "commands/publicationcmds.h" +#include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/trigger.h" @@ -1444,6 +1446,10 @@ doDeletion(const ObjectAddress *object, int flags) RemovePublicationById(object->objectId); break; + case VariableRelationId: + DropVariableById(object->objectId); + break; + case CastRelationId: case CollationRelationId: case ConversionRelationId: diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ed9aeee24bc..df15852d906 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "common/hashfn_unstable.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -224,6 +225,7 @@ static bool TSParserIsVisibleExt(Oid prsId, bool *is_missing); static bool TSDictionaryIsVisibleExt(Oid dictId, bool *is_missing); static bool TSTemplateIsVisibleExt(Oid tmplId, bool *is_missing); static bool TSConfigIsVisibleExt(Oid cfgid, bool *is_missing); +static bool VariableIsVisibleExt(Oid varid, bool *is_missing); static void recomputeNamespacePath(void); static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); @@ -985,6 +987,84 @@ RelationIsVisibleExt(Oid relid, bool *is_missing) return visible; } +/* + * VariableIsVisible + * Determine whether a variable (identified by OID) is visible in the + * current search path. Visible means "would be found by searching + * for the unqualified variable name". + */ +bool +VariableIsVisible(Oid varid) +{ + return VariableIsVisibleExt(varid, NULL); +} + +/* + * VariableIsVisibleExt + * As above, but if the variable isn't found and is_missing is not NULL, + * then set *is_missing = true and return false, instead of throwing + * an error. (Caller must initialize *is_missing = false.) + */ +static bool +VariableIsVisibleExt(Oid varid, bool *is_missing) +{ + HeapTuple vartup; + Form_pg_variable varform; + Oid varnamespace; + bool visible; + + vartup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(vartup)) + { + if (is_missing != NULL) + { + *is_missing = true; + return false; + } + + elog(ERROR, "cache lookup failed for session variable %u", varid); + } + varform = (Form_pg_variable) GETSTRUCT(vartup); + + recomputeNamespacePath(); + + /* + * Quick check: if it ain't in the path at all, it ain't visible. We don't + * expect usage of session variables in the system namespace. + */ + varnamespace = varform->varnamespace; + if (!list_member_oid(activeSearchPath, varnamespace)) + visible = false; + else + { + /* + * If it is in the path, it might still not be visible; it could be + * hidden by another variable of the same name earlier in the path. So + * we must do a slow check for conflicting relations. + */ + char *varname = NameStr(varform->varname); + + visible = false; + foreach_oid(namespaceId, activeSearchPath) + { + if (namespaceId == varnamespace) + { + /* found it first in path */ + visible = true; + break; + } + if (OidIsValid(get_varname_varid(varname, namespaceId))) + { + /* found something else first in path */ + break; + } + } + } + + ReleaseSysCache(vartup); + + return visible; +} /* * TypenameGetTypid @@ -3356,6 +3436,133 @@ TSConfigIsVisibleExt(Oid cfgid, bool *is_missing) return visible; } +/* + * Returns oid of session variable specified by possibly qualified identifier. + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariable(const char *nspname, + const char *varname, + bool missing_ok) +{ + Oid varoid = InvalidOid; + + if (nspname) + { + Oid namespaceId = LookupExplicitNamespace(nspname, missing_ok); + + /* if nspname is a known namespace, the variable must be there */ + if (OidIsValid(namespaceId)) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + } + } + else + { + /* iterate over the schemas on the search_path */ + recomputeNamespacePath(); + + foreach_oid(namespaceId, activeSearchPath) + { + varoid = GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(namespaceId)); + + if (OidIsValid(varoid)) + break; + } + } + + if (!OidIsValid(varoid) && !missing_ok) + { + if (nspname) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s.%s\" does not exist", + nspname, varname))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" does not exist", + varname))); + } + + return varoid; +} + +/* + * Returns oid of session variable specified by possibly qualified identifier + * + * If not found, returns InvalidOid if missing_ok, else throws error. + */ +Oid +LookupVariableFromNameList(List *names, + bool missing_ok) +{ + char *catname = NULL; + char *nspname = NULL; + char *varname = NULL; + + switch (list_length(names)) + { + case 1: + varname = strVal(linitial(names)); + break; + case 2: + nspname = strVal(linitial(names)); + varname = strVal(lsecond(names)); + break; + case 3: + catname = strVal(linitial(names)); + nspname = strVal(lsecond(names)); + varname = strVal(lthird(names)); + + /* check catalog name */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper session variable name (too many dotted names): %s", + NameListToString(names)))); + break; + } + + return LookupVariable(nspname, varname, missing_ok); +} + +/* + * The input list contains names with indirection expressions used as the left + * part of LET statement. The following routine returns a new list with only + * initial strings (names) - without indirection expressions. + */ +List * +NamesFromList(List *names) +{ + ListCell *l; + List *result = NIL; + + foreach(l, names) + { + Node *n = lfirst(l); + + if (IsA(n, String)) + { + result = lappend(result, n); + } + else + break; + } + + return result; +} /* * DeconstructQualifiedName diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index c75b7131ed7..ac1f8a4db3b 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -62,6 +62,7 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -635,6 +636,20 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_USER_MAPPING, false }, + { + "session variable", + VariableRelationId, + VariableOidIndexId, + VARIABLEOID, + VARIABLENAMENSP, + Anum_pg_variable_oid, + Anum_pg_variable_varname, + Anum_pg_variable_varnamespace, + Anum_pg_variable_varowner, + Anum_pg_variable_varacl, + OBJECT_VARIABLE, + true + } }; /* @@ -830,6 +845,9 @@ static const struct object_type_map }, { "statistics object", OBJECT_STATISTIC_EXT + }, + { + "session variable", OBJECT_VARIABLE } }; @@ -855,6 +873,7 @@ static ObjectAddress get_object_address_attrdef(ObjectType objtype, bool missing_ok); static ObjectAddress get_object_address_type(ObjectType objtype, TypeName *typename, bool missing_ok); +static ObjectAddress get_object_address_variable(List *object, bool missing_ok); static ObjectAddress get_object_address_opcf(ObjectType objtype, List *object, bool missing_ok); static ObjectAddress get_object_address_opf_member(ObjectType objtype, @@ -1126,6 +1145,9 @@ get_object_address(ObjectType objtype, Node *object, missing_ok); address.objectSubId = 0; break; + case OBJECT_VARIABLE: + address = get_object_address_variable(castNode(List, object), missing_ok); + break; /* no default, to let compiler warn about missing case */ } @@ -2101,6 +2123,24 @@ textarray_to_strvaluelist(ArrayType *arr) return list; } +/* + * Find the ObjectAddress for a session variable + */ +static ObjectAddress +get_object_address_variable(List *object, bool missing_ok) +{ + ObjectAddress address; + char *nspname = NULL; + char *varname = NULL; + + ObjectAddressSet(address, VariableRelationId, InvalidOid); + + DeconstructQualifiedName(object, &nspname, &varname); + address.objectId = LookupVariable(nspname, varname, missing_ok); + + return address; +} + /* * SQL-callable version of get_object_address */ @@ -2295,6 +2335,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_TABCONSTRAINT: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: + case OBJECT_VARIABLE: objnode = (Node *) name; break; case OBJECT_ACCESS_METHOD: @@ -2466,6 +2507,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_STATISTIC_EXT: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: if (!object_ownercheck(address.classId, address.objectId, roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, NameListToString(castNode(List, object))); @@ -3495,6 +3537,32 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case VariableRelationId: + { + char *nspname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + if (VariableIsVisible(object->objectId)) + nspname = NULL; + else + nspname = get_namespace_name(varform->varnamespace); + + appendStringInfo(&buffer, _("session variable %s"), + quote_qualified_identifier(nspname, + NameStr(varform->varname))); + + ReleaseSysCache(tup); + break; + } + case TSParserRelationId: { HeapTuple tup; @@ -4669,6 +4737,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "transform"); break; + case VariableRelationId: + appendStringInfoString(&buffer, "session variable"); + break; + default: elog(ERROR, "unsupported object class: %u", object->classId); } @@ -6019,6 +6091,33 @@ getObjectIdentityParts(const ObjectAddress *object, } break; + case VariableRelationId: + { + char *schema; + char *varname; + HeapTuple tup; + Form_pg_variable varform; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", + object->objectId); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + schema = get_namespace_name_or_temp(varform->varnamespace); + varname = NameStr(varform->varname); + + appendStringInfo(&buffer, "%s", + quote_qualified_identifier(schema, varname)); + + if (objname) + *objname = list_make2(schema, pstrdup(varname)); + + ReleaseSysCache(tup); + break; + } + default: elog(ERROR, "unsupported object class: %u", object->classId); } diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 16e3e5c7457..6e3e8813328 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -46,6 +46,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/defrem.h" #include "commands/event_trigger.h" @@ -1714,6 +1715,7 @@ shdepReassignOwned_Owner(Form_pg_shdepend sdepForm, Oid newrole) case DatabaseRelationId: case TSConfigRelationId: case TSDictionaryRelationId: + case VariableRelationId: AlterObjectOwner_internal(sdepForm->classid, sdepForm->objid, newrole); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index cb75e11fced..aaf61a6f61c 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -41,6 +41,7 @@ #include "catalog/pg_ts_dict.h" #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" +#include "catalog/pg_variable.h" #include "commands/alter.h" #include "commands/collationcmds.h" #include "commands/dbcommands.h" @@ -140,6 +141,10 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; + case VariableRelationId: + Assert(OidIsValid(nspOid)); + msgfmt = gettext_noop("session variable \"%s\" already exists in schema \"%s\""); + break; default: elog(ERROR, "unsupported object class: %u", classId); break; @@ -435,6 +440,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_TSTEMPLATE: case OBJECT_PUBLICATION: case OBJECT_SUBSCRIPTION: + case OBJECT_VARIABLE: { ObjectAddress address; Relation catalog; @@ -575,6 +581,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TSDICTIONARY: case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: + case OBJECT_VARIABLE: { Relation catalog; Oid classId; @@ -657,6 +664,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case TSDictionaryRelationId: case TSTemplateRelationId: case TSConfigRelationId: + case VariableRelationId: { Relation catalog; @@ -887,6 +895,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_TABLESPACE: case OBJECT_TSDICTIONARY: case OBJECT_TSCONFIGURATION: + case OBJECT_VARIABLE: { ObjectAddress address; diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63..ebb585dc4a1 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -476,6 +476,10 @@ does_not_exist_skipping(ObjectType objtype, Node *object) msg = gettext_noop("publication \"%s\" does not exist, skipping"); name = strVal(object); break; + case OBJECT_VARIABLE: + msg = gettext_noop("session variable \"%s\" does not exist, skipping"); + name = NameListToString(castNode(List, object)); + break; case OBJECT_COLUMN: case OBJECT_DATABASE: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index f34868da5ab..6b46aeaef59 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -2281,6 +2281,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2364,6 +2366,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c..57b4e6719c2 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_TSPARSER: case OBJECT_TSTEMPLATE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: return false; /* diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..f641e00c1ac --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,88 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "catalog/namespace.h" +#include "catalog/pg_type.h" +#include "commands/session_variable.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Creates a new variable + * + * Used by CREATE VARIABLE command + */ +ObjectAddress +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid namespaceid; + AclResult aclresult; + Oid typid; + int32 typmod; + Oid varowner = GetUserId(); + Oid collation; + Oid typcollation; + ObjectAddress variable; + + namespaceid = + RangeVarGetAndCheckCreationNamespace(stmt->variable, NoLock, NULL); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typid, &typmod); + + /* disallow pseudotypes */ + if (get_typtype(typid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("session variable cannot be pseudo-type %s", + format_type_be(typid)))); + + aclresult = object_aclcheck(TypeRelationId, typid, GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error_type(aclresult, typid); + + typcollation = get_typcollation(typid); + + if (stmt->collClause) + collation = LookupCollation(pstate, + stmt->collClause->collname, + stmt->collClause->location); + else + collation = typcollation; + + /* complain if COLLATE is applied to an uncollatable type */ + if (OidIsValid(collation) && !OidIsValid(typcollation)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("collations are not supported by type %s", + format_type_be(typid)), + parser_errposition(pstate, stmt->collClause->location))); + + variable = create_variable(stmt->variable->relname, + namespaceid, + typid, + typmod, + varowner, + collation, + stmt->if_not_exists); + + elog(DEBUG1, "record for session variable \"%s\" (oid:%d) was created in pg_variable", + stmt->variable->relname, variable.objectId); + + return variable; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index fc89352b661..8891a468cfa 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" @@ -6913,6 +6914,7 @@ ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, * (possibly nested several levels deep in composite types, arrays, etc!). * Eventually, we'd like to propagate the check or rewrite operation * into such tables, but for now, just error out if we find any. + * Also, check if "typeOid" is used as type of some session variable. * * Caller should provide either the associated relation of a rowtype, * or a type name (not both) for use in the error message, if any. @@ -6976,6 +6978,45 @@ find_composite_type_dependencies(Oid typeOid, Relation origRelation, continue; } + /* check if the type is used as type of some session variable */ + if (pg_depend->classid == VariableRelationId) + { + Oid varid = pg_depend->objid; + + if (origTypeName) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + origTypeName, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter foreign table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + else if (origRelation->rd_rel->relkind == RELKIND_RELATION || + origRelation->rd_rel->relkind == RELKIND_MATVIEW || + origRelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter table \"%s\" because session variable \"%s.%s\" uses it", + RelationGetRelationName(origRelation), + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)))); + + continue; + } + /* Else, ignore dependees that aren't relations */ if (pg_depend->classid != RelationRelationId) continue; diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index c6de04819f1..4749f3f7e56 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -53,6 +53,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablecmds.h" #include "commands/typecmds.h" @@ -3391,6 +3392,20 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } continue; } + else if (pg_depend->classid == VariableRelationId) + { + /* + * We cannot to validate constraint inside session variables from + * other sessions, so better to fail if there are any session + * variable, that use this domain. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter domain \"%s\" because session variable \"%s.%s\" uses it", + domainTypeName, + get_namespace_name(get_session_variable_namespace(pg_depend->objid)), + get_session_variable_name(pg_depend->objid)))); + } /* Else, ignore dependees that aren't user columns of relations */ /* (we assume system columns are never of domain types) */ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 9fd48acb1f8..51b8778db5c 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -52,6 +52,7 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_trigger.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/trigger.h" #include "gramparse.h" @@ -284,8 +285,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSeqStmt CreateSessionVarStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -782,8 +783,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1049,6 +1050,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1626,6 +1628,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5308,6 +5311,34 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE VARIABLE qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $3; + n->typeName = $5; + n->collClause = (CollateClause *) $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename opt_collate_clause + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + n->variable = $6; + n->typeName = $8; + n->collClause = (CollateClause *) $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -7119,6 +7150,7 @@ object_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* @@ -10054,6 +10086,24 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN @@ -10415,6 +10465,24 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER VARIABLE any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newschema = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS any_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $5; + n->newschema = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; /***************************************************************************** @@ -10696,6 +10764,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER VARIABLE any_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + n->objectType = OBJECT_VARIABLE; + n->object = (Node *) $3; + n->newowner = $6; + $$ = (Node *)n; + } ; @@ -18045,6 +18121,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18701,6 +18778,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index e96b38a59d5..059d8f7f180 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -105,6 +105,7 @@ typedef struct List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ List *grants; /* GRANT items */ + List *variables; /* CREATE VARIABLE items */ } CreateSchemaStmtContext; @@ -4112,6 +4113,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.indexes = NIL; cxt.triggers = NIL; cxt.grants = NIL; + cxt.variables = NIL; /* * Run through each schema element in the schema element list. Separate @@ -4180,6 +4182,15 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) cxt.grants = lappend(cxt.grants, element); break; + case T_CreateSessionVarStmt: + { + CreateSessionVarStmt *elp = (CreateSessionVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(element)); @@ -4193,6 +4204,7 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) result = list_concat(result, cxt.indexes); result = list_concat(result, cxt.triggers); result = list_concat(result, cxt.grants); + result = list_concat(result, cxt.variables); return result; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 918db53dd5e..1a0e11ba701 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fa7cd7e06a7..1c4031eea23 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -40,6 +40,7 @@ #include "catalog/pg_subscription.h" #include "catalog/pg_transform.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "utils/array.h" @@ -3881,3 +3882,67 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +/* ---------- PG_VARIABLE CACHE ---------- */ + +/* + * get_varname_varid + * Given name and namespace of variable, look up the OID. + */ +Oid +get_varname_varid(const char *varname, Oid varnamespace) +{ + return GetSysCacheOid2(VARIABLENAMENSP, Anum_pg_variable_oid, + PointerGetDatum(varname), + ObjectIdGetDatum(varnamespace)); +} + +/* + * get_session_variable_name + * Returns a palloc'd copy of the name of a given session variable. + */ +char * +get_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = pstrdup(NameStr(varform->varname)); + + ReleaseSysCache(tup); + + return varname; +} + +/* + * get_session_variable_namespace + * Returns the pg_namespace OID associated with a given session variable. + */ +Oid +get_session_variable_namespace(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + Oid varnamespace; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varnamespace = varform->varnamespace; + + ReleaseSysCache(tup); + + return varnamespace; +} diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index f1423f28c32..d12c3b957b7 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -115,6 +115,8 @@ extern Oid TypenameGetTypid(const char *typname); extern Oid TypenameGetTypidExtended(const char *typname, bool temp_ok); extern bool TypeIsVisible(Oid typid); +extern bool VariableIsVisible(Oid varid); + extern FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, @@ -189,6 +191,10 @@ extern SearchPathMatcher *GetSearchPathMatcher(MemoryContext context); extern SearchPathMatcher *CopySearchPathMatcher(SearchPathMatcher *path); extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); +extern List *NamesFromList(List *names); +extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); +extern Oid LookupVariableFromNameList(List *names, bool missing_ok); + extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); extern Oid FindDefaultConversionProc(int32 for_encoding, int32 to_encoding); diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..49f36ac6885 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * sessionvariable.h + * prototypes for sessionvariable.c. + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/session_variable.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SESSIONVARIABLE_H +#define SESSIONVARIABLE_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" + +extern ObjectAddress CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); + +#endif diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f1706df58fd..464cf782b8d 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2370,6 +2370,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW, } ObjectType; @@ -3552,6 +3553,21 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * {Create|Alter} VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + CollateClause *collClause; + bool if_not_exists; /* do nothing if it already exists */ +} CreateSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a1..6f513f04225 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -486,6 +486,7 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d59..ea86954dded 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -68,6 +68,7 @@ PG_CMDTAG(CMDTAG_ALTER_TRANSFORM, "ALTER TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TRIGGER, "ALTER TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_ALTER_TYPE, "ALTER TYPE", true, true, false) PG_CMDTAG(CMDTAG_ALTER_USER_MAPPING, "ALTER USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_VARIABLE, "ALTER VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_ALTER_VIEW, "ALTER VIEW", true, false, false) PG_CMDTAG(CMDTAG_ANALYZE, "ANALYZE", false, false, false) PG_CMDTAG(CMDTAG_BEGIN, "BEGIN", false, false, false) @@ -123,6 +124,7 @@ PG_CMDTAG(CMDTAG_CREATE_TRANSFORM, "CREATE TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TRIGGER, "CREATE TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_CREATE_TYPE, "CREATE TYPE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_USER_MAPPING, "CREATE USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_VARIABLE, "CREATE VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_CREATE_VIEW, "CREATE VIEW", true, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE, "DEALLOCATE", false, false, false) PG_CMDTAG(CMDTAG_DEALLOCATE_ALL, "DEALLOCATE ALL", false, false, false) @@ -175,6 +177,7 @@ PG_CMDTAG(CMDTAG_DROP_TRANSFORM, "DROP TRANSFORM", true, false, false) PG_CMDTAG(CMDTAG_DROP_TRIGGER, "DROP TRIGGER", true, false, false) PG_CMDTAG(CMDTAG_DROP_TYPE, "DROP TYPE", true, false, false) PG_CMDTAG(CMDTAG_DROP_USER_MAPPING, "DROP USER MAPPING", true, false, false) +PG_CMDTAG(CMDTAG_DROP_VARIABLE, "DROP VARIABLE", true, false, false) PG_CMDTAG(CMDTAG_DROP_VIEW, "DROP VIEW", true, false, false) PG_CMDTAG(CMDTAG_EXECUTE, "EXECUTE", false, false, false) PG_CMDTAG(CMDTAG_EXPLAIN, "EXPLAIN", false, false, false) diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 50fb149e9ac..03c4c58580e 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -139,6 +139,7 @@ extern char get_func_prokind(Oid funcid); extern bool get_func_leakproof(Oid funcid); extern RegProcedure get_func_support(Oid funcid); extern Oid get_relname_relid(const char *relname, Oid relnamespace); +extern Oid get_varname_varid(const char *varname, Oid varnamespace); extern char *get_rel_name(Oid relid); extern Oid get_rel_namespace(Oid relid); extern Oid get_rel_type_id(Oid relid); @@ -211,6 +212,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_session_variable_name(Oid varid); +extern Oid get_session_variable_namespace(Oid varid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/test/regress/expected/dependency.out b/src/test/regress/expected/dependency.out index 75a078ada9e..cd8e4412fa9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -151,3 +151,20 @@ owner of type deptest_t DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; +-- should fail +DROP DOMAIN vardomain; +ERROR: cannot drop type vardomain because other objects depend on it +DETAIL: column c of composite type vartype depends on type vardomain +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TYPE vartype; +ERROR: cannot drop type vartype because other objects depend on it +DETAIL: session variable var1 depends on type vartype +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 00000000000..9c7595e9a41 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,163 @@ +SET log_statement TO ddl; +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar01}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + pg_identify_object_as_address +----------------------------------------------------- + ("session variable","{public,ddltest_sesvar02}",{}) +(1 row) + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + pg_identify_object_as_address +------------------------------------------------------------- + ("session variable","{sesvartest_ddl,ddltest_sesvar03}",{}) +(1 row) + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; +-- add new field to composite value is supported, +-- change type of field is prohibited +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ERROR: cannot alter type "sesvartest_type_ddl" because session variable "public.ddltest_sesvar04" uses it +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ERROR: cannot alter table "sesvartest_table_ddl" because session variable "public.ddltest_sesvar06" uses it +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); +ERROR: cannot alter domain "sesvartest_domain_ddl" because session variable "public.ddltest_sesvar05" uses it +-- should fail +DROP TYPE sesvartest_type_ddl; +ERROR: cannot drop type sesvartest_type_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar04 depends on type sesvartest_type_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP DOMAIN sesvartest_domain_ddl; +ERROR: cannot drop type sesvartest_domain_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar05 depends on type sesvartest_domain_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP TABLE sesvartest_table_ddl; +ERROR: cannot drop table sesvartest_table_ddl because other objects depend on it +DETAIL: session variable ddltest_sesvar06 depends on type sesvartest_table_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); +DROP VARIABLE ddltest_sesvar04; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar04 name={public,ddltest_sesvar04} args={} +DROP VARIABLE ddltest_sesvar05; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar05 name={public,ddltest_sesvar05} args={} +DROP VARIABLE ddltest_sesvar06; +NOTICE: NORMAL: orig=t normal=f istemp=f type=session variable identity=public.ddltest_sesvar06 name={public,ddltest_sesvar06} args={} +-- should to fail +DROP SCHEMA sesvartest_ddl; +ERROR: cannot drop schema sesvartest_ddl because other objects depend on it +DETAIL: session variable sesvartest_ddl.ddltest_sesvar03 depends on schema sesvartest_ddl +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; +NOTICE: drop cascades to session variable sesvartest_ddl.ddltest_sesvar03 +NOTICE: NORMAL: orig=f normal=t istemp=f type=session variable identity=sesvartest_ddl.ddltest_sesvar03 name={sesvartest_ddl,ddltest_sesvar03} args={} +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; +DROP FUNCTION svar_event_trigger_report_dropped(); +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; + obj_description +------------------------------- + some session variable comment +(1 row) + +DROP VARIABLE ddltest_sesvar07; +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; +CREATE ROLE regress_variable_owner_ddl; +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; +SET ROLE TO regress_variable_owner_ddl; +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; +ERROR: must be owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +SET ROLE TO DEFAULT; +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; +-- should fail +DROP ROLE regress_variable_owner_ddl; +ERROR: role "regress_variable_owner_ddl" cannot be dropped because some objects depend on it +DETAIL: owner of session variable sesvartest_ddl.ddltest_sesvar08_renamed +privileges for schema sesvartest_ddl +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; +ERROR: session variable "ddltest_sesvar08_renamed" does not exist +SET SEARCH_PATH TO 'sesvartest_ddl'; +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; +SET SEARCH_PATH TO DEFAULT; +SET ROLE TO DEFAULT; +DROP SCHEMA sesvartest_ddl; +DROP ROLE regress_variable_owner_ddl; +SET log_statement TO DEFAULT; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +NOTICE: session variable "ddltest_sesvar09" already exists, skipping +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +NOTICE: session variable "ddltest_sesvar09" does not exist, skipping +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ERROR: session variable "sesvar10" already exists +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; +ERROR: session variable "sesvar10" already exists in schema "svartest01_ddl" +DROP SCHEMA svartest01_ddl CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to session variable svartest01_ddl.sesvar10 +drop cascades to session variable svartest01_ddl.sesvar11 +DROP SCHEMA svartest02_ddl CASCADE; +NOTICE: drop cascades to session variable svartest02_ddl.sesvar10 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..3abc1aca5f2 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -115,7 +115,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # NB: temp.sql does reconnects which transiently use 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml session_variables_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/dependency.sql b/src/test/regress/sql/dependency.sql index 8d74ed7122c..6c18b7f840a 100644 --- a/src/test/regress/sql/dependency.sql +++ b/src/test/regress/sql/dependency.sql @@ -114,3 +114,17 @@ DROP USER regress_dep_user2; DROP OWNED BY regress_dep_user2, regress_dep_user0; DROP USER regress_dep_user2; DROP USER regress_dep_user0; + +-- dependency on type +CREATE DOMAIN vardomain AS int; +CREATE TYPE vartype AS (a int, b int, c vardomain); +CREATE VARIABLE var1 AS vartype; + +-- should fail +DROP DOMAIN vardomain; +DROP TYPE vartype; + +-- clean up +DROP VARIABLE var1; +DROP TYPE vartype; +DROP DOMAIN vardomain; diff --git a/src/test/regress/sql/session_variables_ddl.sql b/src/test/regress/sql/session_variables_ddl.sql new file mode 100644 index 00000000000..f844469ecb1 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,150 @@ +SET log_statement TO ddl; + +CREATE VARIABLE ddltest_sesvar01 AS int; +CREATE VARIABLE public.ddltest_sesvar02 AS int; +CREATE SCHEMA sesvartest_ddl; +CREATE VARIABLE sesvartest_ddl.ddltest_sesvar03 AS int; + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{ddltest_sesvar01}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{public,ddltest_sesvar02}', '{}'); + +SELECT pg_identify_object_as_address(classid, objid, objsubid) + FROM pg_get_object_address('session variable', '{sesvartest_ddl,ddltest_sesvar03}', '{}'); + +DROP VARIABLE ddltest_sesvar01; +DROP VARIABLE public.ddltest_sesvar02; + +CREATE TYPE sesvartest_type_ddl AS (a int, b int); +CREATE DOMAIN sesvartest_domain_ddl AS int; +CREATE TABLE sesvartest_table_ddl (a int, b int); + +/* prefix ddltest_ should not be used ever in another tests */ +CREATE VARIABLE ddltest_sesvar04 AS sesvartest_type_ddl; +CREATE VARIABLE ddltest_sesvar05 AS sesvartest_domain_ddl; +CREATE VARIABLE ddltest_sesvar06 AS sesvartest_table_ddl; + +-- add new field to composite value is supported, +-- change type of field is prohibited + +-- should be ok +ALTER TYPE sesvartest_type_ddl ADD ATTRIBUTE c int; +ALTER TABLE sesvartest_table_ddl ADD COLUMN c int; + +-- should fail +ALTER TYPE sesvartest_type_ddl ALTER ATTRIBUTE b TYPE numeric; +ALTER TABLE sesvartest_table_ddl ALTER COLUMN b TYPE numeric; +ALTER DOMAIN sesvartest_domain_ddl ADD CHECK(value <> 100); + +-- should fail +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check event trigger support +CREATE OR REPLACE FUNCTION svar_event_trigger_report_dropped() +RETURNS event_trigger +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + IF r.classid = 'pg_variable'::regclass AND + r.address_names[2] like 'ddltest_sesvar%' + THEN + RAISE NOTICE + 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.address_names, r.address_args; + END IF; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER svar_regress_event_trigger_report_dropped ON sql_drop + WHEN TAG IN ('DROP VARIABLE', 'DROP SCHEMA') + EXECUTE PROCEDURE svar_event_trigger_report_dropped(); + +DROP VARIABLE ddltest_sesvar04; +DROP VARIABLE ddltest_sesvar05; +DROP VARIABLE ddltest_sesvar06; + +-- should to fail +DROP SCHEMA sesvartest_ddl; + +-- should be ok +DROP SCHEMA sesvartest_ddl CASCADE; + +DROP EVENT TRIGGER svar_regress_event_trigger_report_dropped; + +DROP FUNCTION svar_event_trigger_report_dropped(); + +-- should be ok +DROP TYPE sesvartest_type_ddl; +DROP DOMAIN sesvartest_domain_ddl; +DROP TABLE sesvartest_table_ddl; + +-- check comment on variable +CREATE VARIABLE ddltest_sesvar07 AS int; +COMMENT ON VARIABLE ddltest_sesvar07 IS 'some session variable comment'; +SELECT pg_catalog.obj_description(oid, 'pg_variable') FROM pg_variable WHERE varname = 'ddltest_sesvar07'; +DROP VARIABLE ddltest_sesvar07; + +CREATE VARIABLE ddltest_sesvar08 AS int; +ALTER VARIABLE ddltest_sesvar08 RENAME TO ddltest_sesvar08_renamed; + +CREATE SCHEMA sesvartest_ddl; +ALTER VARIABLE ddltest_sesvar08_renamed SET SCHEMA sesvartest_ddl; + +CREATE ROLE regress_variable_owner_ddl; + +GRANT ALL ON SCHEMA sesvartest_ddl TO regress_variable_owner_ddl; + +SET ROLE TO regress_variable_owner_ddl; + +-- should fail +DROP VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed; + +SET ROLE TO DEFAULT; + +ALTER VARIABLE sesvartest_ddl.ddltest_sesvar08_renamed OWNER TO regress_variable_owner_ddl; + +-- should fail +DROP ROLE regress_variable_owner_ddl; + +-- should fail - not on search path +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO 'sesvartest_ddl'; + +-- should be ok +DROP VARIABLE ddltest_sesvar08_renamed; + +SET SEARCH_PATH TO DEFAULT; + +SET ROLE TO DEFAULT; + +DROP SCHEMA sesvartest_ddl; + +DROP ROLE regress_variable_owner_ddl; + +SET log_statement TO DEFAULT; + +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +CREATE VARIABLE IF NOT EXISTS ddltest_sesvar09 AS int; +DROP VARIABLE IF EXISTS ddltest_sesvar09; +DROP VARIABLE IF EXISTS ddltest_sesvar09; + +CREATE SCHEMA svartest01_ddl CREATE VARIABLE sesvar10 AS int; +CREATE VARIABLE svartest01_ddl.sesvar11 AS int; +CREATE SCHEMA svartest02_ddl CREATE VARIABLE sesvar10 AS int; + +-- should to fail +CREATE VARIABLE svartest01_ddl.sesvar10 AS int; +ALTER VARIABLE svartest01_ddl.sesvar11 RENAME TO sesvar10; +ALTER VARIABLE svartest02_ddl.sesvar10 SET SCHEMA svartest01_ddl; + +DROP SCHEMA svartest01_ddl CASCADE; +DROP SCHEMA svartest02_ddl CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 51f18720deb..e35d9d7dfb0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -560,6 +560,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext -- 2.51.0 [text/x-patch] v20250929-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20250929-0001-introduce-new-class-catalog-pg_variable.patch) download | inline diff: From aeb45b3bbf86dcce1b6b272400264eb9b997dd2b Mon Sep 17 00:00:00 2001 From: "[email protected]" <[email protected]> Date: Wed, 28 May 2025 08:30:25 +0200 Subject: [PATCH 01/15] introduce new class (catalog) pg_variable This table holds metadata about session variables created by command CREATE VARIABLE, and dropped by command DROP VARIABLE. --- doc/src/sgml/catalogs.sgml | 133 ++++++++++++++++++++ src/backend/catalog/Makefile | 1 + src/backend/catalog/meson.build | 1 + src/backend/catalog/pg_variable.c | 168 +++++++++++++++++++++++++ src/include/catalog/Makefile | 3 +- src/include/catalog/meson.build | 1 + src/include/catalog/pg_variable.h | 96 ++++++++++++++ src/test/regress/expected/oidjoins.out | 4 + src/tools/pgindent/typedefs.list | 2 + 9 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/backend/catalog/pg_variable.c create mode 100644 src/include/catalog/pg_variable.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index e9095bedf21..487a58eda74 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry> <entry>mappings of users to foreign servers</entry> </row> + + <row> + <entry><link linkend="catalog-pg-variable"><structname>pg_variable</structname></link></entry> + <entry>session variables</entry> + </row> </tbody> </tgroup> </table> @@ -9810,4 +9815,132 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l </table> </sect1> + <sect1 id="catalog-pg-variable"> + <title><structname>pg_variable</structname></title> + + <indexterm zone="catalog-pg-variable"> + <primary>pg_variable</primary> + </indexterm> + + <para> + The catalog <structname>pg_variable</structname> stores information about + session variables. + </para> + + <table> + <title><structname>pg_variable</structname> Columns</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + Column Type + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>oid</structfield> <type>oid</type> + </para> + <para> + Row identifier + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartype</structfield> <type>oid</type> + (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the variable's data type + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcreate_lsn</structfield> <type>pg_lsn</type> + </para> + <para> + LSN of the transaction where the variable was created. + <structfield>varcreate_lsn</structfield> and + <structfield>oid</structfield> together form the all-time unique + identifier (<structfield>oid</structfield> alone is not enough, since + object identifiers can get reused). + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varname</structfield> <type>name</type> + </para> + <para> + Name of the session variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varnamespace</structfield> <type>oid</type> + (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The OID of the namespace that contains this variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varowner</structfield> <type>oid</type> + (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>) + </para> + <para> + Owner of the variable + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>vartypmod</structfield> <type>int4</type> + </para> + <para> + <structfield>vartypmod</structfield> records type-specific data + supplied at variable creation time (for example, the maximum + length of a <type>varchar</type> column). It is passed to + type-specific input functions and length coercion functions. + The value will generally be -1 for types that do not need <structfield>vartypmod</structfield>. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varcollation</structfield> <type>oid</type> + (references <link linkend="catalog-pg-collation"><structname>pg_collation</structname></link>.<structfield>oid</structfield>) + </para> + <para> + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + </para></entry> + </row> + + <row> + <entry role="catalog_table_entry"><para role="column_definition"> + <structfield>varacl</structfield> <type>aclitem[]</type> + </para> + <para> + Access privileges; see + <xref linkend="sql-grant"/> and + <xref linkend="sql-revoke"/> + for details + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + </sect1> </chapter> diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08..2c20d60db19 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -45,6 +45,7 @@ OBJS = \ pg_shdepend.o \ pg_subscription.o \ pg_type.o \ + pg_variable.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a..ed44c877fca 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -32,6 +32,7 @@ backend_sources += files( 'pg_shdepend.c', 'pg_subscription.c', 'pg_type.c', + 'pg_variable.c', 'storage.c', 'toasting.c', ) diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c new file mode 100644 index 00000000000..bd6a29a79e5 --- /dev/null +++ b/src/backend/catalog/pg_variable.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.c + * session variables + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/catalog/pg_variable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/dependency.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_variable.h" +#include "utils/builtins.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" + +/* + * Creates entry in pg_variable table + */ +ObjectAddress +create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists) +{ + NameData varname; + bool nulls[Natts_pg_variable]; + Datum values[Natts_pg_variable]; + Relation rel; + HeapTuple tup; + TupleDesc tupdesc; + ObjectAddress myself, + referenced; + ObjectAddresses *addrs; + Oid varid; + + Assert(varName); + Assert(OidIsValid(varNamespace)); + Assert(OidIsValid(varType)); + Assert(OidIsValid(varOwner)); + + rel = table_open(VariableRelationId, RowExclusiveLock); + + /* + * Check for duplicates. Note that this does not really prevent + * duplicates, it's here just to provide nicer error message in common + * case. The real protection is the unique key on the catalog. + */ + if (SearchSysCacheExists2(VARIABLENAMENSP, + PointerGetDatum(varName), + ObjectIdGetDatum(varNamespace))) + { + if (if_not_exists) + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + varName))); + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + varName))); + + table_close(rel, RowExclusiveLock); + + return InvalidObjectAddress; + } + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + + namestrcpy(&varname, varName); + + varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); + + values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); + values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); + values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); + values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); + values[Anum_pg_variable_vartypmod - 1] = Int32GetDatum(varTypmod); + values[Anum_pg_variable_varowner - 1] = ObjectIdGetDatum(varOwner); + values[Anum_pg_variable_varcollation - 1] = ObjectIdGetDatum(varCollation); + + nulls[Anum_pg_variable_varacl - 1] = true; + + tupdesc = RelationGetDescr(rel); + + tup = heap_form_tuple(tupdesc, values, nulls); + CatalogTupleInsert(rel, tup); + Assert(OidIsValid(varid)); + + addrs = new_object_addresses(); + + ObjectAddressSet(myself, VariableRelationId, varid); + + /* dependency on namespace */ + ObjectAddressSet(referenced, NamespaceRelationId, varNamespace); + add_exact_object_address(&referenced, addrs); + + /* dependency on used type */ + ObjectAddressSet(referenced, TypeRelationId, varType); + add_exact_object_address(&referenced, addrs); + + /* dependency on collation */ + if (OidIsValid(varCollation) && + varCollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, varCollation); + add_exact_object_address(&referenced, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + free_object_addresses(addrs); + + /* dependency on owner */ + recordDependencyOnOwner(VariableRelationId, varid, varOwner); + + /* dependency on extension */ + recordDependencyOnCurrentExtension(&myself, false); + + heap_freetuple(tup); + + /* post creation hook for new function */ + InvokeObjectPostCreateHook(VariableRelationId, varid, 0); + + table_close(rel, RowExclusiveLock); + + return myself; +} + +/* + * Drop variable by OID + */ +void +DropVariableById(Oid varid) +{ + Relation rel; + HeapTuple tup; + + rel = table_open(VariableRelationId, RowExclusiveLock); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + CatalogTupleDelete(rel, &tup->t_self); + + ReleaseSysCache(tup); + + table_close(rel, RowExclusiveLock); +} diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe3..f98760de635 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -81,7 +81,8 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_variable.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6f..81398efa7c9 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -69,6 +69,7 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_variable.h', ] # The .dat files we need can just be listed alphabetically. diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h new file mode 100644 index 00000000000..15f530894c5 --- /dev/null +++ b/src/include/catalog/pg_variable.h @@ -0,0 +1,96 @@ +/*------------------------------------------------------------------------- + * + * pg_variable.h + * definition of session variables system catalog (pg_variables) + * + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_variable.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VARIABLE_H +#define PG_VARIABLE_H + +#include "access/xlogdefs.h" +#include "catalog/genbki.h" +#include "catalog/objectaddress.h" +#include "catalog/pg_variable_d.h" +#include "utils/acl.h" + +/* ---------------- + * pg_variable definition. cpp turns this into + * typedef struct FormData_pg_variable + * ---------------- + */ +CATALOG(pg_variable,9222,VariableRelationId) +{ + Oid oid; /* oid */ + + /* OID of entry in pg_type for variable's type */ + Oid vartype BKI_LOOKUP(pg_type); + + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + + /* variable name */ + NameData varname; + + /* OID of namespace containing variable class */ + Oid varnamespace BKI_LOOKUP(pg_namespace); + + /* variable owner */ + Oid varowner BKI_LOOKUP(pg_authid); + + /* typmod for variable's type */ + int32 vartypmod BKI_DEFAULT(-1); + + /* variable collation */ + Oid varcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation); + + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* access permissions */ + aclitem varacl[1] BKI_DEFAULT(_null_); + +#endif +} FormData_pg_variable; + +/* ---------------- + * Form_pg_variable corresponds to a pointer to a tuple with + * the format of the pg_variable relation. + * ---------------- + */ +typedef FormData_pg_variable *Form_pg_variable; + +DECLARE_TOAST(pg_variable, 9223, 9224); + +DECLARE_UNIQUE_INDEX_PKEY(pg_variable_oid_index, 9225, VariableOidIndexId, pg_variable, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_variable_varname_nsp_index, 9226, VariableNameNspIndexId, pg_variable, btree(varname name_ops, varnamespace oid_ops)); + +MAKE_SYSCACHE(VARIABLENAMENSP, pg_variable_varname_nsp_index, 8); +MAKE_SYSCACHE(VARIABLEOID, pg_variable_oid_index, 8); + +extern ObjectAddress create_variable(const char *varName, + Oid varNamespace, + Oid varType, + int32 varTypmod, + Oid varOwner, + Oid varCollation, + bool if_not_exists); + +extern void DropVariableById(Oid varid); + +#endif /* PG_VARIABLE_H */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3..d9953321402 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -266,3 +266,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_variable {vartype} => pg_type {oid} +NOTICE: checking pg_variable {varnamespace} => pg_namespace {oid} +NOTICE: checking pg_variable {varowner} => pg_authid {oid} +NOTICE: checking pg_variable {varcollation} => pg_collation {oid} diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 37f26f6c6b7..51f18720deb 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -909,6 +909,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -968,6 +969,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData -- 2.51.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
end of thread, other threads:[~2025-09-29 10:13 UTC | newest] Thread overview: 24+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2024-11-16 14:27 Re: proposal: schema variables Dmitry Dolgov <[email protected]> 2024-11-16 22:49 ` Pavel Stehule <[email protected]> 2024-11-17 04:41 ` Pavel Stehule <[email protected]> 2024-11-19 19:14 ` Pavel Stehule <[email protected]> 2024-11-19 21:30 ` Pavel Stehule <[email protected]> 2024-11-20 07:25 ` Pavel Stehule <[email protected]> 2025-05-21 07:12 ` Pavel Stehule <[email protected]> 2025-05-21 21:22 ` Bruce Momjian <[email protected]> 2025-06-03 07:26 ` Pavel Stehule <[email protected]> 2025-06-03 11:43 ` Pavel Stehule <[email protected]> 2025-06-04 20:22 ` Pavel Stehule <[email protected]> 2025-06-10 14:25 ` Pavel Stehule <[email protected]> 2025-06-13 04:53 ` Pavel Stehule <[email protected]> 2025-06-25 17:33 ` Pavel Stehule <[email protected]> 2025-07-11 19:55 ` Pavel Stehule <[email protected]> 2025-07-22 05:53 ` Pavel Stehule <[email protected]> 2025-07-23 15:25 ` Pavel Stehule <[email protected]> 2025-08-05 05:30 ` Pavel Stehule <[email protected]> 2025-08-13 19:24 ` Pavel Stehule <[email protected]> 2025-08-29 07:03 ` Pavel Stehule <[email protected]> 2025-09-13 09:28 ` Pavel Stehule <[email protected]> 2025-09-15 08:21 ` Jim Jones <[email protected]> 2025-09-15 16:55 ` Pavel Stehule <[email protected]> 2025-09-29 10:13 ` Pavel Stehule <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox