public inbox for [email protected]
help / color / mirror / Atom feedFrom: Pavel Stehule <[email protected]>
To: Jim Jones <[email protected]>
Cc: Bruce Momjian <[email protected]>
Cc: Dmitry Dolgov <[email protected]>
Cc: Laurenz Albe <[email protected]>
Cc: Erik Rijkers <[email protected]>
Cc: Michael Paquier <[email protected]>
Cc: Amit Kapila <[email protected]>
Cc: DUVAL REMI <[email protected]>
Cc: PostgreSQL Hackers <[email protected]>
Cc: jian he <[email protected]>
Cc: Alvaro Herrera <[email protected]>
Cc: PegoraroF10 <[email protected]>
Subject: Re: proposal: schema variables
Date: Mon, 10 Nov 2025 07:40:30 +0100
Message-ID: <CAFj8pRDmCF05tVDbzkwL45WHLhdZnbkQLn9HA3RA-5LGu=wEXg@mail.gmail.com> (raw)
In-Reply-To: <CAFj8pRAOTQ5BkKvCFHkUE=HWzirZhVzxE5iugbcOsz=z5R0ogA@mail.gmail.com>
References: <CAFj8pRDJ9cq00VYSHxs6LsoHNWjhYXyWWBtV6UgeWwhs0AHa9A@mail.gmail.com>
<CAFj8pRCRDhQobRx5FAZgK8drzA_T-9c67U_-wZJ2fmqwEVpFSQ@mail.gmail.com>
<CAFj8pRB1VKs8DbymheidYXDC2i1ZShV+40J7h2j8GDfqM+yLbQ@mail.gmail.com>
<CAFj8pRAMaiRSRyye9LdOTh_TgbwmohuXh7cCS9eSLtOUzCJU5Q@mail.gmail.com>
<CAFj8pRBNj_+Q2CQjpURm5copeY27YPJz=Oa+rpRfa5DHu-j1iQ@mail.gmail.com>
<CAFj8pRDiKxpZA1+dkGK-YW_H8oYp2hVOuFpOPHEkCBc5O4Z_ug@mail.gmail.com>
<CAFj8pRAdUF6etyOK3UsAPu-1nuOpv6aHbFJuhtJO73uxC6vObw@mail.gmail.com>
<CAFj8pRDdhYR66dNVz9WKKHDL64QK+CuqYM3yfiMR_Dp-uOm0bg@mail.gmail.com>
<CAFj8pRDJdF=Jv6M1_ePV8tnzxBfyYHfHodeifhOUyMt1N-fxRQ@mail.gmail.com>
<CAFj8pRAwp0ES9BKXbNocG_pYcqP0Cs2GdtGx_-2XsEe3fkCgdg@mail.gmail.com>
<CAFj8pRAhnp63fV5m7ATr2_nW9_yVbFzwhL4Be8oTY_M0jksrXg@mail.gmail.com>
<CAFj8pRBykx1Oh1JVzY5HfcZPjbGE5PH=DyPN4sM7fBafnNkLkA@mail.gmail.com>
<CAFj8pRBx4w3QS0D2W3yc8hWXH7hynsOwO4x+iv4AstmB6Dmkgw@mail.gmail.com>
<CAFj8pRBeF5Tz_DiKWokeEcRxrJR5FFDbTKhaaB-9TMYU3RYDfw@mail.gmail.com>
<[email protected]>
<CAFj8pRBDVrm_Ju9RJe1J1Yhb5BPtOq49cQH7RvQ_ZPuh__Nrcw@mail.gmail.com>
<CAFj8pRAe1BN1iLoVaD4CLtMY4pg2XROVqmU8HngqBaFRhF4zCg@mail.gmail.com>
<CAFj8pRBryCY4LqtrPu9E_mtZYkNfZb64-MetWE0232whdOLgjA@mail.gmail.com>
<CAFj8pRCPjHT8xfnMHRroc_wx4wBT4ocqzjH--jqo0xHqec4qhA@mail.gmail.com>
<CAFj8pRBTmSPUPET-Jo=REsLLx15kmHqYXmXwL2BKLeZjZqUUEg@mail.gmail.com>
<CAFj8pRAOTQ5BkKvCFHkUE=HWzirZhVzxE5iugbcOsz=z5R0ogA@mail.gmail.com>
Hi
fresh rebase
Regards
Pavel
Attachments:
[text/x-patch] v20251110-0014-memory-cleaning-after-DROP-VARIABLE.patch (23.4K, 3-v20251110-0014-memory-cleaning-after-DROP-VARIABLE.patch)
download | inline diff:
From 30b5945ab434b281c65f73320b31fe05d650fba9 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 c7f6424b76b..bb063bb7bbc 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.1
[text/x-patch] v20251110-0015-plpgsql-tests.patch (13.6K, 4-v20251110-0015-plpgsql-tests.patch)
download | inline diff:
From 0f2196c4fc10a1436f32164415896fae4bfa2889 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.1
[text/x-patch] v20251110-0013-DISCARD-VARIABLES.patch (10.2K, 5-v20251110-0013-DISCARD-VARIABLES.patch)
download | inline diff:
From dd3d2a8a8ebac2ffc82dc408a2407e67b978d596 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 3bafa701509..c7f6424b76b 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 738d1391684..851c307704c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2147,7 +2147,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 0ea7a166977..aecb357f027 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2967,6 +2967,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 fe020a9b381..a4597abad05 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -4241,7 +4241,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 0fcce621c34..112440fa8b0 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -4107,6 +4107,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 be14059cc79..bc2a81cbf2f 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.1
[text/x-patch] v20251110-0012-function-pg_get_session_variables_memory-for-cleanin.patch (4.9K, 6-v20251110-0012-function-pg_get_session_variables_memory-for-cleanin.patch)
download | inline diff:
From 31f6143a8d66ad2339257b306f36958263f43b85 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 9bdc92bfd30..3bafa701509 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 00aefeda5e9..4c62ac61104 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12627,4 +12627,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 => '8068', 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.1
[text/x-patch] v20251110-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch (57.5K, 7-v20251110-0011-LET-command-assign-a-result-of-expression-to-the-ses.patch)
download | inline diff:
From 4c4c9377da10d7df0b33d13958a064b23ce0ad91 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 | 136 ++++++++-
src/backend/parser/gram.y | 39 ++-
src/backend/parser/parse_agg.c | 7 +
src/backend/parser/parse_expr.c | 26 +-
src/backend/parser/parse_func.c | 3 +
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/parser/parse_node.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 +
31 files changed, 1043 insertions(+), 32 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 910989a7aae..6cba31aaefc 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5695,10 +5695,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 61db22a4c25..e392dc8d084 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 0fe33a7efcb..5b3be89f732 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..9bdc92bfd30 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, NULL);
+
+ /*
+ * 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 4c5b101c8ee..76f478ff580 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 c761fde6acb..0d5461cbd6b 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4374,6 +4374,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 cc03b0311a4..8dccd50737a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -376,6 +376,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
@@ -621,6 +635,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 a12a3c6094c..0cbe09834c5 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -2249,6 +2249,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;
}
@@ -3734,7 +3755,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)
@@ -3836,9 +3857,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)
@@ -3861,6 +3883,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 c3897aafd21..4038876497d 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -52,15 +52,18 @@
#include "utils/builtins.h"
#include "utils/guc.h"
#include "utils/rel.h"
+#include "utils/lsyscache.h"
#include "utils/syscache.h"
-/* Passthrough data for transformPLAssignStmtTarget */
+/* Passthrough data for transformAssignTarget */
typedef struct SelectStmtPassthrough
{
- PLAssignStmt *stmt; /* the assignment statement */
+ Node *stmt; /* the assignment statement */
Node *target; /* node representing the target variable */
+ char *target_name; /* the name used by err */
List *indirection; /* indirection yet to be applied to target */
+ CoercionContext ccontext; /* context indicators to control coercions */
} SelectStmtPassthrough;
/* Hook for plugins to get control at end of parse analysis */
@@ -84,7 +87,7 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt);
static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
static Query *transformPLAssignStmt(ParseState *pstate,
PLAssignStmt *stmt);
-static List *transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
+static List *transformAssignTarget(ParseState *pstate, List *tlist,
SelectStmtPassthrough *passthru);
static Query *transformDeclareCursorStmt(ParseState *pstate,
DeclareCursorStmt *stmt);
@@ -94,6 +97,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 +346,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 +426,11 @@ transformStmt(ParseState *pstate, Node *parseTree)
(CallStmt *) parseTree);
break;
+ case T_LetStmt:
+ result = transformLetStmt(pstate,
+ (LetStmt *) parseTree);
+ break;
+
default:
/*
@@ -481,6 +492,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree)
case T_ExplainStmt:
case T_CreateTableAsStmt:
case T_CallStmt:
+ case T_LetStmt:
result = true;
break;
@@ -546,6 +558,7 @@ query_requires_rewrite_plan(Query *query)
case T_ExplainStmt:
case T_CreateTableAsStmt:
case T_CallStmt:
+ case T_LetStmt:
result = true;
break;
default:
@@ -1389,7 +1402,7 @@ count_rowexpr_columns(ParseState *pstate, Node *expr)
*
* This function is also used to transform the source expression of a
* PLAssignStmt. In that usage, passthru is non-NULL and we need to
- * call transformPLAssignStmtTarget after the initial transformation of the
+ * call transformAssignTarget after the initial transformation of the
* SELECT's targetlist. (We could generalize this into an arbitrary callback
* function, but for now that would just be more notation with no benefit.)
* All the rest is the same as a regular SelectStmt.
@@ -1442,8 +1455,8 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt,
* Otherwise, mark column origins (which are useless in a PLAssignStmt).
*/
if (passthru)
- qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList,
- passthru);
+ qry->targetList = transformAssignTarget(pstate, qry->targetList,
+ passthru);
else
markTargetListOrigins(pstate, qry->targetList);
@@ -2841,9 +2854,11 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
EXPR_KIND_UPDATE_TARGET);
/* Set up passthrough data for transformPLAssignStmtTarget */
- passthru.stmt = stmt;
+ passthru.stmt = (Node *) stmt;
passthru.target = target;
+ passthru.target_name = stmt->name;
passthru.indirection = indirection;
+ passthru.ccontext = COERCION_PLPGSQL;
/*
* To avoid duplicating a lot of code, we use transformSelectStmt to do
@@ -2866,18 +2881,21 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt)
/*
* Callback function to adjust a SELECT's tlist to make the output suitable
- * for assignment to a PLAssignStmt's target variable.
+ * for assignment to a PLAssignStmt's target variable pr LET's target
+ * session variable.
*
* Note: we actually modify the tle->expr in-place, but the function's API
* is set up to not presume that.
*/
static List *
-transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
- SelectStmtPassthrough *passthru)
+transformAssignTarget(ParseState *pstate, List *tlist,
+ SelectStmtPassthrough *passthru)
{
- PLAssignStmt *stmt = passthru->stmt;
+ Node *stmt = passthru->stmt;
Node *target = passthru->target;
+ char *target_name = passthru->target_name;
List *indirection = passthru->indirection;
+ CoercionContext ccontext = passthru->ccontext;
Oid targettype;
int32 targettypmod;
Oid targetcollation;
@@ -2912,7 +2930,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
tle->expr = (Expr *)
transformAssignmentIndirection(pstate,
target,
- stmt->name,
+ target_name,
false,
targettype,
targettypmod,
@@ -2920,10 +2938,10 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
indirection,
list_head(indirection),
(Node *) tle->expr,
- COERCION_PLPGSQL,
+ ccontext,
exprLocation(target));
}
- else if (targettype != type_id &&
+ else if (IsA(stmt, PLAssignStmt) && targettype != type_id &&
(targettype == RECORDOID || ISCOMPLEX(targettype)) &&
(type_id == RECORDOID || ISCOMPLEX(type_id)))
{
@@ -2946,7 +2964,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
coerce_to_target_type(pstate,
orig_expr, type_id,
targettype, targettypmod,
- COERCION_PLPGSQL,
+ ccontext,
COERCE_IMPLICIT_CAST,
-1);
/* With COERCION_PLPGSQL, this error is probably unreachable */
@@ -2955,7 +2973,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("variable \"%s\" is of type %s"
" but expression is of type %s",
- stmt->name,
+ target_name,
format_type_be(targettype),
format_type_be(type_id)),
errhint("You will need to rewrite or cast the expression."),
@@ -3319,6 +3337,92 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt)
return result;
}
+/*
+ * transformLetStmt -
+ * transform an Let Statement
+ */
+static Query *
+transformLetStmt(ParseState *pstate, LetStmt *stmt)
+{
+ Query *qry;
+ Query *result;
+ Node *target;
+ List *names = NULL;
+ int nnames;
+ List *indirection;
+ VariableFence *vf;
+ SelectStmtPassthrough passthru;
+ Param *paramvar;
+
+ /* gram allows only SELECT */
+ Assert(IsA(stmt->query, SelectStmt));
+
+ names = NamesFromList(stmt->target);
+ nnames = list_length(names);
+
+ /* Use implicit VariableFence for forcing session variables */
+ vf = makeNode(VariableFence);
+ vf->varname = names;
+ vf->location = stmt->location;
+
+ target = transformExpr(pstate, (Node *) vf, EXPR_KIND_LET_TARGET);
+
+ /*
+ * The FieldStore is returned when field name is specified.
+ * In this case decrease nnames number (number of dotted names),
+ * and get target from FieldSelect node. Field name will be part
+ * of indirection.
+ */
+ if (IsA(target, FieldStore))
+ {
+ nnames -= 1;
+ target = (Node *)((FieldStore *) target)->arg;
+ }
+
+ paramvar = castNode(Param, target);
+
+ Assert(paramvar->paramkind == PARAM_VARIABLE);
+
+ if (list_length(stmt->target) > nnames)
+ indirection = list_copy_tail(stmt->target, nnames);
+ else
+ indirection = NIL;
+
+ /*
+ * The parameter used as basenode has to have special mark,
+ * because requires special access when we do SELECT access check.
+ */
+ if (indirection)
+ {
+ paramvar->parambasenode = true;
+ pstate->p_hasSessionVariables = true;
+ }
+
+ /* Set up passthrough data for transformAssignTarget */
+ passthru.stmt = (Node *) stmt;
+ passthru.target = (Node *) paramvar;
+ passthru.target_name = get_session_variable_name(paramvar->paramvarid);
+ passthru.indirection = indirection;
+ passthru.ccontext = COERCION_ASSIGNMENT;
+
+ /* we need to postpone conversion of "unknown" to text */
+ pstate->p_resolve_unknowns = false;
+
+ qry = transformSelectStmt(pstate, (SelectStmt *) stmt->query, &passthru);
+
+ qry->resultVariable = paramvar->paramvarid;
+ qry->canSetTag = true;
+
+ stmt->query = (Node *) qry;
+
+ /* 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 f5c2f151752..738d1391684 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -302,7 +302,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
@@ -748,7 +748,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 LET LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED LSN_P
MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
@@ -1095,6 +1095,7 @@ stmt:
| ImportForeignSchemaStmt
| IndexStmt
| InsertStmt
+ | LetStmt
| ListenStmt
| RefreshMatViewStmt
| LoadStmt
@@ -12983,6 +12984,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:
@@ -18099,6 +18132,7 @@ unreserved_keyword:
| LARGE_P
| LAST_P
| LEAKPROOF
+ | LET
| LEVEL
| LISTEN
| LOAD
@@ -18716,6 +18750,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 3254c83cc6c..ddf7905654c 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -580,6 +580,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
errkind = true;
break;
+ 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
@@ -996,6 +1000,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ 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 8b32f74de23..ff16a3bc3cc 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -593,6 +593,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
case EXPR_KIND_PARTITION_BOUND:
err = _("cannot use column reference in partition bound expression");
break;
+ case EXPR_KIND_LET_TARGET:
+ err = _("cannot use column reference as target of LET command");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -963,6 +966,7 @@ expr_kind_allows_session_variables(ParseExprKind p_expr_kind)
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
case EXPR_KIND_VALUES_SINGLE:
+ case EXPR_KIND_LET_TARGET:
result = true;
break;
@@ -1998,6 +2002,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_GENERATED_COLUMN:
err = _("cannot use subquery in column generation expression");
break;
+ case EXPR_KIND_LET_TARGET:
+ err = _("cannot use subquery as a target of LET command");
+ break;
/*
* There is intentionally no default: case here, so that the
@@ -3261,7 +3268,8 @@ make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg)
}
/*
- * Generate param variable for reference to session variable
+ * Generate param variable for reference to session variable. It can be
+ * wrapped by FieldSelect (Read) or FieldStore (Write) nodes.
*/
static Node *
makeParamSessionVariable(ParseState *pstate,
@@ -3285,6 +3293,20 @@ makeParamSessionVariable(ParseState *pstate,
TupleDesc tupdesc;
int i;
+ if (pstate->p_expr_kind == EXPR_KIND_LET_TARGET)
+ {
+ FieldStore *fstore = makeNode(FieldStore);
+
+ /*
+ * make aux FieldStore wrapper - it is used just for signalization,
+ * so session variable has an attribute. All necessary transformations
+ * and checks will be done in transformAssignmentIndirection.
+ */
+ fstore->arg = (Expr *) param;
+
+ return (Node *) fstore;
+ }
+
tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true);
if (!tupdesc)
ereport(ERROR,
@@ -3426,6 +3448,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "GENERATED AS";
case EXPR_KIND_CYCLE_MARK:
return "CYCLE";
+ 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 778d69c6f3c..13616c9b3c2 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
case EXPR_KIND_CYCLE_MARK:
errkind = true;
break;
+ case EXPR_KIND_LET_TARGET:
+ errkind = true;
+ break;
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 760d0faaad0..0ea7a166977 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -236,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
@@ -1065,6 +1066,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,
@@ -2212,6 +2218,10 @@ UtilityContainsQuery(Node *parsetree)
return UtilityContainsQuery(qry->utilityStmt);
return qry;
+ case T_LetStmt:
+ qry = castNode(Query, ((LetStmt *) parsetree)->query);
+ return qry;
+
default:
return NULL;
}
@@ -2410,6 +2420,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:
{
@@ -3299,6 +3313,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 094e65db849..fe020a9b381 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1272,8 +1272,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",
@@ -4820,6 +4820,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 a4a9e281463..0fcce621c34 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 */
@@ -2170,6 +2173,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 75a863dad93..50d4a20be77 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -192,6 +192,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 0aa52cbc4c5..cf863c43d8c 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -161,6 +161,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 5f3a1a2d1be..9107891fa9d 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 d1ad6861805..badd1618af8 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -258,6 +258,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 84e886940d8..026743b7337 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -82,6 +82,7 @@ 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_LET_TARGET, /* only session variables */
} ParseExprKind;
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index cbb09fcf511..be14059cc79 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 c022959ecca..d3a9433b432 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.51.1
[text/x-patch] v20251110-0010-svariableReceiver.patch (9.0K, 8-v20251110-0010-svariableReceiver.patch)
download | inline diff:
From b4565c5ed413f0a11b74a23f417c16b83e0e7490 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 | 173 +++++++++++++++++++++++
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, 206 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..aadd446f277
--- /dev/null
+++ b/src/backend/executor/svariableReceiver.c
@@ -0,0 +1,173 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "access/htup_details.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 113e79c33fa..c022959ecca 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2667,6 +2667,7 @@ STRLEN
SV
SVariableData
SVariable
+SVariableState
SYNCHRONIZATION_BARRIER
SYSTEM_INFO
SampleScan
--
2.51.1
[text/x-patch] v20251110-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch (19.5K, 9-v20251110-0009-fill-an-auxiliary-buffer-with-values-of-session-vari.patch)
download | inline diff:
From c0445dc1e749a3b58e86d09b0856586295518837 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 27c9eec697b..4c5b101c8ee 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 18ae8f0d4bb..b6048d96900 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 d8000da3f86..860c526edbe 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 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 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 315c76ac03f..113e79c33fa 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2725,6 +2725,7 @@ SerializedTransactionState
Session
SessionBackupState
SessionEndType
+SessionVariableValue
SetConstraintState
SetConstraintStateData
SetConstraintTriggerData
--
2.51.1
[text/x-patch] v20251110-0008-collect-session-variables-used-in-plan-and-assign-pa.patch (16.4K, 10-v20251110-0008-collect-session-variables-used-in-plan-and-assign-pa.patch)
download | inline diff:
From fd2ce4187816f9ac19fe405078e010db812044cc 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 c4fd646b999..cc03b0311a4 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -374,6 +374,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
@@ -617,6 +618,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;
@@ -805,6 +809,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name,
*/
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 ccdc9bc264a..a12a3c6094c 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);
/*****************************************************************************
@@ -1341,6 +1344,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
@@ -2041,8 +2088,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.
@@ -2132,6 +2180,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);
+ }
}
/*
@@ -2141,6 +2196,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)
@@ -2159,6 +2218,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);
}
@@ -2220,7 +2313,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
@@ -2242,7 +2338,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);
}
@@ -3635,6 +3732,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 481d8011791..2355a7ce922 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1647,6 +1647,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 81d768ff2a2..55c2ec50c95 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
@@ -4822,7 +4852,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 0fe63c6bb83..d26e744e296 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 30d889b54c5..75a863dad93 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -189,6 +189,9 @@ typedef struct PlannerGlobal
/* extension state */
void **extension_state pg_node_attr(read_write_ignore);
int extension_state_allocated;
+
+ /* list of used session variables */
+ List *sessionVariables;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
@@ -547,6 +550,8 @@ struct PlannerInfo
bool hasRecursion;
/* true if a planner extension may replan this subquery */
bool assumeReplanning;
+ /* 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 c4393a94321..0aa52cbc4c5 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -158,6 +158,9 @@ typedef struct PlannedStmt
*/
List *extension_state;
+ /* 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 00addf15992..fb81ceb375f 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -132,4 +132,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.1
[text/x-patch] v20251110-0007-local-HASHTAB-for-currently-used-session-variables-a.patch (14.1K, 11-v20251110-0007-local-HASHTAB-for-currently-used-session-variables-a.patch)
download | inline diff:
From 89d5ccd1110dfb961d1031c4b969c2a112aacb27 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 8f697bb2266..b29e3dde1ac 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1732,8 +1732,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 7f427231602..315c76ac03f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2665,6 +2665,8 @@ SSL_CTX
STARTUPINFO
STRLEN
SV
+SVariableData
+SVariable
SYNCHRONIZATION_BARRIER
SYSTEM_INFO
SampleScan
--
2.51.1
[text/x-patch] v20251110-0005-support-of-session-variables-for-pg_dump.patch (15.3K, 12-v20251110-0005-support-of-session-variables-for-pg_dump.patch)
download | inline diff:
From 278b583095202f44211d60a71c74d101305dcad7 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 4e7303ea631..aea83e50a7b 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 relations");
getSubscriptionRelations(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 2d22723aa91..0bf5453ad7a 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 a00918bacb4..702ac7fa1c1 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);
@@ -5694,6 +5695,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,
@@ -11828,6 +12011,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 */
@@ -16333,6 +16519,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",
@@ -20151,6 +20340,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 72a00e1bc20..f94e05a05f1 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,
@@ -746,6 +747,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
*/
@@ -830,5 +844,6 @@ extern void getPublicationTables(Archive *fout, TableInfo tblinfo[],
int numTables);
extern void getSubscriptions(Archive *fout);
extern void getSubscriptionRelations(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 164c76e0864..9f6ff6b7d57 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,
@@ -1750,6 +1752,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 445a541abf6..d5a70b39cc6 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -750,6 +750,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
@@ -1776,6 +1786,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) '
@@ -4074,6 +4101,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
@@ -4538,6 +4582,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 78cc8372d33..00291b4f38e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3221,6 +3221,7 @@ VarString
VarStringSortSupport
Variable
VariableAssignHook
+VariableInfo
VariableSetKind
VariableSetStmt
VariableShowStmt
--
2.51.1
[text/x-patch] v20251110-0006-session-variable-fences-parsing.patch (32.2K, 13-v20251110-0006-session-variable-fences-parsing.patch)
download | inline diff:
From a1b07a3d5f3615fbb0906cceea6a3b3fc3aea8ae 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 98d7c2120b1..910989a7aae 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5688,6 +5688,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 800f37dc2ea..b20d2074382 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -3567,6 +3567,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 ede838cd40c..c761fde6acb 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1669,6 +1669,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;
@@ -4701,6 +4704,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 3b392b084ad..c3897aafd21 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);
@@ -1526,6 +1528,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)
{
@@ -1752,6 +1755,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);
@@ -2003,6 +2007,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)
{
@@ -2475,6 +2480,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);
@@ -2542,6 +2548,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 1510551e03c..f5c2f151752 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -532,7 +532,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
@@ -888,7 +888,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 '*' '/' '%'
@@ -15770,6 +15770,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
@@ -17172,6 +17185,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 32d6ae918ca..8b32f74de23 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -16,6 +16,7 @@
#include "postgres.h"
#include "access/htup_details.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_type.h"
#include "miscadmin.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,
@@ -107,7 +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);
/*
* transformExpr -
@@ -371,6 +375,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));
@@ -904,6 +912,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)
@@ -3123,6 +3260,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 556ab057e5a..5f3c9904af8 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);
@@ -8816,6 +8818,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.
*
@@ -13601,6 +13611,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 e2b452e31c9..a4a9e281463 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);
@@ -322,6 +324,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 1b4436f2ff6..5f3a1a2d1be 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 00291b4f38e..7f427231602 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3213,6 +3213,7 @@ ValidatorValidateCB
ValuesScan
ValuesScanState
Var
+VariableFence
VarBit
VarChar
VarParamState
--
2.51.1
[text/x-patch] v20251110-0004-support-of-session-variables-for-psql.patch (22.6K, 14-v20251110-0004-support-of-session-variables-for-psql.patch)
download | inline diff:
From 1d89fd39dff6d968d3118ebb0075944d1ca49652 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 c00506c44cb..7f97cbbd3b0 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 f56c70263e0..0e9c2ead73c 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 ab837a3cb9e..800f37dc2ea 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -5362,3 +5362,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 4a2976dddf0..5d10646771a 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 36f24502842..12bc383e83e 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");
@@ -5348,6 +5350,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 ec0b49b957b..97baf95d2eb 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 316a2dafbf1..094e65db849 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -998,6 +998,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
@@ -1362,6 +1369,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 */
};
@@ -1929,7 +1937,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",
@@ -2643,6 +2651,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");
@@ -3249,7 +3260,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"))
@@ -4074,6 +4085,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 */
@@ -4351,6 +4369,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);
@@ -4552,7 +4576,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",
@@ -4560,6 +4586,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",
@@ -4574,7 +4601,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"))
@@ -4582,7 +4610,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");
/*
* Complete "GRANT/REVOKE * ON DATABASE/DOMAIN/..." with a list of
@@ -4624,6 +4653,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
@@ -4950,7 +4981,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
@@ -5439,6 +5470,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 dccccbde484..00aefeda5e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6717,6 +6717,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 c8f3932edf0..db2141bb606 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -6057,6 +6057,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
@@ -6278,6 +6302,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
@@ -6512,6 +6542,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"
@@ -6681,6 +6717,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
@@ -6814,6 +6856,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"
@@ -6861,6 +6909,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 dcdbd4fc020..61ca2bb60ee 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1661,6 +1661,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
@@ -1772,6 +1785,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"
@@ -1813,6 +1829,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"
@@ -1853,6 +1871,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"
@@ -1877,6 +1896,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"
@@ -1902,6 +1922,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.1
[text/x-patch] v20251110-0003-GRANT-REVOKE-variable.patch (54.4K, 15-v20251110-0003-GRANT-REVOKE-variable.patch)
download | inline diff:
From 0ee964caf4d0ca47196b1759ee65a8f30447bfe2 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 7c79ddc9c09..7a6d25e2920 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -3363,7 +3363,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 76c91ea461d..98d7c2120b1 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -2317,6 +2317,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>
@@ -2352,6 +2353,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>
@@ -2596,7 +2599,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>
@@ -2611,7 +2615,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>
@@ -2798,6 +2803,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>
@@ -5667,6 +5678,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 d4508114a48..c00506c44cb 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 043f5d5a40a..b76be52ea7e 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 09d16c1bd05..1510551e03c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -791,7 +791,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
WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
@@ -8087,6 +8087,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));
@@ -8132,6 +8140,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;
+ }
;
@@ -8330,6 +8346,7 @@ defacl_privilege_target:
| TYPES_P { $$ = OBJECT_TYPE; }
| SCHEMAS { $$ = OBJECT_SCHEMA; }
| LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; }
+ | VARIABLES { $$ = OBJECT_VARIABLE; }
;
@@ -18232,6 +18249,7 @@ unreserved_keyword:
| VALIDATOR
| VALUE_P
| VARIABLE
+ | VARIABLES
| VARYING
| VERSION_P
| VIEW
@@ -18891,6 +18909,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 5cf9e12fcb9..dccccbde484 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 8b1f2bd3b68..d1ad6861805 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -490,6 +490,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 1bcf69031da..d8000da3f86 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 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 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.1
[text/x-patch] v20251110-0002-CREATE-DROP-ALTER-VARIABLE.patch (80.4K, 16-v20251110-0002-CREATE-DROP-ALTER-VARIABLE.patch)
download | inline diff:
From f67b10d5d5026fb7ecba3a32a33c56f5f802d80e 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 948b9327f24..76c91ea461d 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -5654,6 +5654,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 a76cf5c383f..8f697bb2266 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1726,6 +1726,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 b561b4cc580..de10c356fe1 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 e167406c744..61db22a4c25 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 8d81244910b..5336bf4eae6 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 2cf02c37b17..0fe33a7efcb 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 d23474da4fb..ab837a3cb9e 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
@@ -3359,6 +3439,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 f99acfd2b4b..29fe872dc16 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 9f640ad4810..beecf3c8adf 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 3aac459e483..123074f59df 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"
@@ -6912,6 +6913,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.
@@ -6975,6 +6977,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 5979580139f..08666ff7a8b 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"
@@ -3392,6 +3393,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 57fe0186547..09d16c1bd05 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"
@@ -290,8 +291,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
@@ -789,8 +790,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
WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
@@ -1056,6 +1057,7 @@ stmt:
| CreatePolicyStmt
| CreatePLangStmt
| CreateSchemaStmt
+ | CreateSessionVarStmt
| CreateSeqStmt
| CreateStmt
| CreateSubscriptionStmt
@@ -1634,6 +1636,7 @@ schema_stmt:
| CreateTrigStmt
| GrantStmt
| ViewStmt
+ | CreateSessionVarStmt
;
@@ -5336,6 +5339,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 ]
@@ -7147,6 +7178,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; }
;
/*
@@ -10082,6 +10114,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
@@ -10443,6 +10493,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;
+ }
;
/*****************************************************************************
@@ -10724,6 +10792,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;
+ }
;
@@ -18155,6 +18231,7 @@ unreserved_keyword:
| VALIDATE
| VALIDATOR
| VALUE_P
+ | VARIABLE
| VARYING
| VERSION_P
| VIEW
@@ -18813,6 +18890,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 082967c0a86..760d0faaad0 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"
@@ -183,6 +184,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:
@@ -1388,6 +1390,10 @@ ProcessUtilitySlow(ParseState *pstate,
}
break;
+ case T_CreateSessionVarStmt:
+ address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree);
+ break;
+
/*
* ************* object creation / destruction **************
*/
@@ -2347,6 +2353,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;
@@ -2655,6 +2664,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;
}
@@ -3235,6 +3247,10 @@ CreateCommandTag(Node *parsetree)
}
break;
+ case T_CreateSessionVarStmt:
+ tag = CMDTAG_CREATE_VARIABLE;
+ break;
+
default:
elog(WARNING, "unrecognized node type: %d",
(int) nodeTag(parsetree));
@@ -3773,6 +3789,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 d14294a4ece..e2b452e31c9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2373,6 +2373,7 @@ typedef enum ObjectType
OBJECT_TSTEMPLATE,
OBJECT_TYPE,
OBJECT_USER_MAPPING,
+ OBJECT_VARIABLE,
OBJECT_VIEW,
} ObjectType;
@@ -3555,6 +3556,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 5d4fe27ef96..8b1f2bd3b68 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -489,6 +489,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 c4606d65043..cbb09fcf511 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 a0f5fab0f5d..1bcf69031da 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 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_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 cf5c87c03a2..78cc8372d33 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.51.1
[text/x-patch] v20251110-0001-introduce-new-class-catalog-pg_variable.patch (16.3K, 17-v20251110-0001-introduce-new-class-catalog-pg_variable.patch)
download | inline diff:
From a7513b34828091b6608b3abf93c15efaea9a800d 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 6c8a0f173c9..7c79ddc9c09 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>
@@ -9834,4 +9839,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 432509277c9..cf5c87c03a2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -911,6 +911,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
@@ -970,6 +971,7 @@ Form_pg_ts_parser
Form_pg_ts_template
Form_pg_type
Form_pg_user_mapping
+Form_pg_variable
FormatNode
FreeBlockNumberArray
FreeListData
--
2.51.1
view thread (439+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: proposal: schema variables
In-Reply-To: <CAFj8pRDmCF05tVDbzkwL45WHLhdZnbkQLn9HA3RA-5LGu=wEXg@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox