From 5a0237794c7f9eaae4ff50f8b1a851f1958ad37b Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 6c8a0f173c97..7c79ddc9c096 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -369,6 +369,11 @@ pg_user_mapping mappings of users to foreign servers + + + pg_variable + session variables + @@ -9834,4 +9839,132 @@ SCRAM-SHA-256$<iteration count>:&l + + <structname>pg_variable</structname> + + + pg_variable + + + + The catalog pg_variable stores information about + session variables. + + + + <structname>pg_variable</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + vartype oid + (references pg_type.oid) + + + The OID of the variable's data type + + + + + + varcreate_lsn pg_lsn + + + LSN of the transaction where the variable was created. + varcreate_lsn and + oid together form the all-time unique + identifier (oid alone is not enough, since + object identifiers can get reused). + + + + + + varname name + + + Name of the session variable + + + + + + varnamespace oid + (references pg_namespace.oid) + + + The OID of the namespace that contains this variable + + + + + + varowner oid + (references pg_authid.oid) + + + Owner of the variable + + + + + + vartypmod int4 + + + vartypmod records type-specific data + supplied at variable creation time (for example, the maximum + length of a varchar 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 vartypmod. + + + + + + varcollation oid + (references pg_collation.oid) + + + The defined collation of the variable, or zero if the variable is + not of a collatable data type. + + + + + + varacl aclitem[] + + + Access privileges; see + and + + for details + + + + + +
+
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08d..2c20d60db19c 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 1958ea9238a7..ed44c877fca4 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 000000000000..bd6a29a79e58 --- /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 2bbc7805fe37..f98760de635a 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 ec1cf467f6fa..81398efa7c9b 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 000000000000..15f530894c5a --- /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 215eb899be3e..d99533214021 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 018b5919cf66..acf090b1ccf8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -910,6 +910,7 @@ FormData_pg_ts_parser FormData_pg_ts_template FormData_pg_type FormData_pg_user_mapping +FormData_pg_variable FormExtraData_pg_attribute Form_pg_aggregate Form_pg_am @@ -969,6 +970,7 @@ Form_pg_ts_parser Form_pg_ts_template Form_pg_type Form_pg_user_mapping +Form_pg_variable FormatNode FreeBlockNumberArray FreeListData From 4ceb1e7bfc98381a1063c51304b6030c746395d2 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 65bc070d2e5f..fa711a09bc47 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5362,6 +5362,27 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; + + Session Variables + + + Session variables + + + + session variable + + + + Session variables are database objects that can hold a value. + + + + The session variable holds value in session memory. This value is private + to each session and is released when the session ends. + + + Other Database Objects diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 8651f0cdb919..c37fd5da50b1 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1711,6 +1711,21 @@ + + Session variable + + + 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. + + + For more information, see . + + + + Shared memory diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b89..1e4c43b8b614 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; + + + <command>Packages and package variables</command> + + + The PL/pgSQL language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + PostgreSQL. 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 ). + + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867ab..2f67de3e21b6 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. + @@ -99,6 +100,7 @@ Complete list of usable sgml source files in this directory. + @@ -147,6 +149,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml new file mode 100644 index 000000000000..96d2586423e4 --- /dev/null +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -0,0 +1,178 @@ + + + + + ALTER VARIABLE + + + + session variable + altering + + + + ALTER VARIABLE + 7 + SQL - Language Statements + + + + ALTER VARIABLE + + change the definition of a session variable + + + + + +ALTER VARIABLE name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } +ALTER VARIABLE name RENAME TO new_name +ALTER VARIABLE name SET SCHEMA new_schema + + + + + Description + + + The ALTER VARIABLE command changes the definition of an + existing session variable. There are several subforms: + + + + OWNER + + + This form changes the owner of the session variable. + + + + + + RENAME + + + This form changes the name of the session variable. + + + + + + SET SCHEMA + + + This form moves the session variable into another schema. + + + + + + + + + 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 CREATE 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 CREATE 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. + + + + + Parameters + + + + + name + + + The name (possibly schema-qualified) of the existing session variable + to alter. + + + + + + new_owner + + + The user name of the new owner of the session variable. + + + + + + new_name + + + The new name for the session variable. + + + + + + new_schema + + + The new schema for the session variable. + + + + + + + + + Examples + + + To rename a session variable: + +ALTER VARIABLE foo RENAME TO boo; + + + + + To change the owner of the session variable boo to + joe: + +ALTER VARIABLE boo OWNER TO joe; + + + + + To change the schema of the session variable boo to + private: + +ALTER VARIABLE boo SET SCHEMA private; + + + + + + Compatibility + + + Session variables and this command in particular are a PostgreSQL extension. + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b1335..21cd80818fbe 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -65,6 +65,7 @@ COMMENT ON TRANSFORM FOR type_name LANGUAGE lang_name | TRIGGER trigger_name ON table_name | TYPE object_name | + VARIABLE object_name | VIEW object_name } IS { string_literal | NULL } diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6c..d2bb265209b1 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 role_sp schema. Currently, only CREATE TABLE, CREATE VIEW, CREATE INDEX, CREATE SEQUENCE, CREATE - TRIGGER and GRANT are accepted as clauses - within CREATE SCHEMA. Other kinds of objects may - be created in separate commands after the schema is created. + TRIGGER, GRANT and CREATE + VARIABLE are accepted as clauses within CREATE + SCHEMA. Other kinds of objects may be created in separate + commands after the schema is created. @@ -214,6 +215,11 @@ CREATE VIEW hollywood.winners AS The IF NOT EXISTS option is a PostgreSQL extension. + + + The CREATE VARIABLE command is a + PostgreSQL extension. + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 000000000000..6e988f2e4729 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ + + + + + CREATE VARIABLE + + + + session variable + defining + + + + CREATE VARIABLE + 7 + SQL - Language Statements + + + + CREATE VARIABLE + define a session variable + + + + +CREATE VARIABLE [ IF NOT EXISTS ] name [ AS ] data_type [ COLLATE collation ] + + + + Description + + + The CREATE VARIABLE command creates a session variable. + Session variables, like relations, exist within a schema and their access is + controlled via the commands GRANT and REVOKE. + + + + 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 LET command. + The content of a session variable is not transactional. This is the same as + regular variables in procedural languages. + + + + Session variables are retrieved by the SELECT + command. Their value is set with the LET command. + + + + + Session variables can be shadowed by other identifiers. + For details, see . + + + + + + Parameters + + + + + IF NOT EXISTS + + + Do not throw an error if the name already exists. A notice is issued in + this case. + + + + + + name + + + The name, optionally schema-qualified, of the session variable. + + + + + + data_type + + + The name, optionally schema-qualified, of the data type of the session + variable. + + + + + + COLLATE collation + + + The COLLATE 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. + + + + + + + + + Notes + + + Use the DROP VARIABLE command to remove a session + variable. + + + + + Examples + + + Create an date session variable var1: + +CREATE VARIABLE var1 AS date; + + + + + + + Compatibility + + + The CREATE VARIABLE command is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 000000000000..5bdb3560f0b0 --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,117 @@ + + + + + DROP VARIABLE + + + + session variable + removing + + + + DROP VARIABLE + 7 + SQL - Language Statements + + + + DROP VARIABLE + remove a session variable + + + + +DROP VARIABLE [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP VARIABLE removes a session variable. + A session variable can only be removed by its owner or a superuser. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the session variable does not exist. A notice is + issued in this case. + + + + + + name + + + The name, optionally schema-qualified, of a session variable. + + + + + + CASCADE + + + Automatically drop objects that depend on the session variable (such as + views), and in turn all objects that depend on those objects + (see ). + + + + + + RESTRICT + + + Refuse to drop the session variable if any objects depend on it. This is + the default. + + + + + + + + Examples + + + To remove the session variable var1: + + +DROP VARIABLE var1; + + + + + Compatibility + + + The DROP VARIABLE command is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc4..25578f3946cd 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 cd139bd65a66..00e3630e0ec6 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 7dded634eb81..1d62e63d4f7f 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 d23474da4fb2..ab837a3cb9e4 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 c75b7131ed70..ac1f8a4db3bb 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 16e3e5c7457d..6e3e88133282 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 cb2fbdc7c601..aee40e7bd598 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 cb75e11fced6..aaf61a6f61c4 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 ceb9a229b63b..ebb585dc4a1e 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 f34868da5ab9..6b46aeaef592 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 dd4cde41d32c..101c8d75dd1e 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 cee5d7bbb9c7..57b4e6719c25 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 000000000000..f641e00c1acc --- /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 3aac459e483d..123074f59dfe 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 5979580139fc..08666ff7a8b6 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 8a0470d5b841..bb7b3d33dfb3 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 WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -1056,6 +1057,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1633,6 +1635,7 @@ schema_stmt: | CreateTrigStmt | GrantStmt | ViewStmt + | CreateSessionVarStmt ; @@ -5335,6 +5338,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 ] @@ -7146,6 +7177,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; } ; /* @@ -10081,6 +10113,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 @@ -10442,6 +10492,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; + } ; /***************************************************************************** @@ -10723,6 +10791,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; + } ; @@ -18133,6 +18209,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18789,6 +18866,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 e96b38a59d50..059d8f7f1805 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 918db53dd5e7..1a0e11ba7017 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -182,6 +183,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -1380,6 +1382,10 @@ ProcessUtilitySlow(ParseState *pstate, } break; + case T_CreateSessionVarStmt: + address = CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + /* * ************* object creation / destruction ************** */ @@ -2333,6 +2339,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = CMDTAG_ALTER_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_ALTER_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; break; @@ -2641,6 +2650,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_STATISTIC_EXT: tag = CMDTAG_DROP_STATISTICS; break; + case OBJECT_VARIABLE: + tag = CMDTAG_DROP_VARIABLE; + break; default: tag = CMDTAG_UNKNOWN; } @@ -3217,6 +3229,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3751,6 +3767,10 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index fa7cd7e06a7a..1c4031eea23c 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 f1423f28c326..d12c3b957b7a 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 000000000000..49f36ac68857 --- /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 ecbddd12e1b3..6dde01f2c7b7 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 84182eaaae2a..5e6d3aff8af9 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -488,6 +488,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 d250a714d597..ea86954dded7 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 50fb149e9ac9..03c4c58580e9 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 75a078ada9e1..cd8e4412fa92 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 000000000000..9c7595e9a417 --- /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 a0f5fab0f5df..1bcf69031da4 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 8d74ed7122c2..6c18b7f840a1 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 000000000000..f844469ecb1f --- /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 acf090b1ccf8..fc5a27c81f7c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -561,6 +561,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext From 7b3e429bedcca0aaf2d5d8bc207b1b33d071def4 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 7c79ddc9c096..7a6d25e29202 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -3363,7 +3363,8 @@ SCRAM-SHA-256$<iteration count>:&l f = function, T = type, n = schema, - L = large object + L = large object, + V = session variable diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fa711a09bc47..420a4d9ff112 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -2025,6 +2025,7 @@ REVOKE ALL ON accounts FROM PUBLIC; For sequences, this privilege also allows use of the currval function. For large objects, this privilege allows the object to be read. + For session variables, this privilege allows the object to be read. @@ -2060,6 +2061,8 @@ REVOKE ALL ON accounts FROM PUBLIC; setval 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. @@ -2304,7 +2307,8 @@ REVOKE ALL ON accounts FROM PUBLIC; LARGE OBJECT, SEQUENCE, TABLE (and table-like objects), - table column + table column, + SESSION VARIABLE @@ -2319,7 +2323,8 @@ REVOKE ALL ON accounts FROM PUBLIC; LARGE OBJECT, SEQUENCE, TABLE, - table column + table column, + SESSION VARIABLE @@ -2506,6 +2511,12 @@ REVOKE ALL ON accounts FROM PUBLIC; U \dT+ + + SESSION VARIABLE + rw + none + \dV+ + @@ -5375,6 +5386,10 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; Session variables are database objects that can hold a value. + Session variables, like relations, exist within a schema and their access + is controlled via GRANT and REVOKE + commands. A session variable can be created by the CREATE + VARIABLE command. diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c6..a57f7665054f 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'); + + + + has_session_variable_privilege + + has_session_variable_privilege ( + user name or oid, + session_variable text or oid, + privilege text ) + boolean + + + Does user have privilege for session variable? + Allowable privilege types are + SELECT, and + UPDATE. + + + @@ -1089,8 +1108,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for TABLESPACE, 'F' for FOREIGN DATA WRAPPER, 'S' for FOREIGN SERVER, - or - 'T' for TYPE or DOMAIN. + 'T' for TYPE or DOMAIN or + 'V' for SESSION VARIABLE. diff --git a/doc/src/sgml/ref/alter_default_privileges.sgml b/doc/src/sgml/ref/alter_default_privileges.sgml index 6acd0f1df914..bc73817061fe 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 ] role_name | PUBLIC } [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + TO { [ GROUP ] role_name | 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 ] role_name | PUBLIC } [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON VARIABLES + FROM { [ GROUP ] role_name | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] @@ -129,14 +142,14 @@ REVOKE [ GRANT OPTION FOR ] 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 FUNCTIONS and ROUTINES are - equivalent in this command. (ROUTINES is preferred - going forward as the standard term for functions and procedures taken - together. In earlier PostgreSQL releases, only the - word FUNCTIONS 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 FUNCTIONS and + ROUTINES are equivalent in this command. + (ROUTINES is preferred going forward as the standard term + for functions and procedures taken together. In earlier PostgreSQL releases, + only the word FUNCTIONS was allowed. It is not possible + to set default privileges for functions and procedures separately.) diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c00..c11860fa200f 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -101,6 +101,12 @@ GRANT role_name [, ...] TO role_specification ] +GRANT { SELECT | UPDATE | ALL [ PRIVILEGES ] } + ON { VARIABLE variable_name [, ...] + | ALL VARIABLES IN SCHEMA schema_name [, ...] } + TO role_specification [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY role_specification ] + where role_specification can be: [ GROUP ] role_name @@ -119,8 +125,8 @@ GRANT role_name [, ...] TO @@ -236,9 +242,9 @@ GRANT role_name [, ...] TO 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. ALL - TABLES also affects views and foreign tables, just like the - specific-object GRANT command. ALL + only for tables, sequences, functions, procedures and variables. + ALL TABLES also affects views and foreign tables, just + like the specific-object GRANT command. ALL FUNCTIONS also affects aggregate and window functions, but not procedures, again just like the specific-object GRANT command. Use ALL ROUTINES to include procedures. @@ -518,8 +524,8 @@ GRANT admins TO joe; - Privileges on databases, tablespaces, schemas, languages, and - configuration parameters are + Privileges on databases, tablespaces, schemas, languages, session variables + and configuration parameters are PostgreSQL extensions. diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1c..760fddb7c209 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 role_specification ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE variable_name [, ...] + | ALL VARIABLES IN SCHEMA schema_name [, ...] } + FROM { role_specification | PUBLIC } [, ...] + [ GRANTED BY role_specification ] + [ CASCADE | RESTRICT ] + where role_specification can be: [ GROUP ] role_name diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 00e3630e0ec6..93c10beebe9f 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 ac1f8a4db3bb..32cd078c1627 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 bd6a29a79e58..d8ede4fa8c86 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 bb7b3d33dfb3..e831142efe22 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 WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -8086,6 +8086,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)); @@ -8131,6 +8139,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; + } ; @@ -8329,6 +8345,7 @@ defacl_privilege_target: | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } | LARGE_P OBJECTS_P { $$ = OBJECT_LARGEOBJECT; } + | VARIABLES { $$ = OBJECT_VARIABLE; } ; @@ -18210,6 +18227,7 @@ unreserved_keyword: | VALIDATOR | VALUE_P | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -18867,6 +18885,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 fbcd64a2609e..9828382b9005 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 ce6e5098eaf0..087d35b943d7 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 9121a382f76b..1ea3f51841c8 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 5e6d3aff8af9..7e7d262b1bb0 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -489,6 +489,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 01ae5b719fd7..5e1a8a82e90c 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 000000000000..f2219529916f --- /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 1bcf69031da4..d8000da3f865 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 000000000000..4b3653cd4ed5 --- /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; From 95705bf61ae21a2a14daf91f4d54900955386889 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 a57f7665054f..3aad9d0529fe 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? + + + + + pg_variable_is_visible + + pg_variable_is_visible ( variable oid ) + boolean + + + Is session variable visible in search path? + + diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7e96a8e1ddb7..1a599884e3cc 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 + + \dV[+] [ pattern ] + + + Lists session variables. + If pattern is + specified, only session variables whose names match the pattern are listed. + If the form \dV+ is used, additional information + about each variable is shown, like access privileges and description. + + + + \du[Sx+] [ pattern ] diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ab837a3cb9e4..800f37dc2ea0 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 4a2976dddf06..5d10646771a5 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 36f245028429..12bc383e83e2 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 @@ listSchemas(const char *pattern, bool verbose, bool showSystem) 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 18ecaa60949d..55ced4aab7b0 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 45d20ea57bc6..d611ea7550e7 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 316a2dafbf1e..094e65db8496 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 */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW */ 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 with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE 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 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 1ea3f51841c8..8de4d9cfa760 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 c8f3932edf09..db2141bb606a 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 dcdbd4fc0209..61ca2bb60eea 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; From a05a2dc6f63778534c8bc22f255f2e899517bb7d Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 4e7303ea6317..aea83e50a7b1 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 2d22723aa911..0bf5453ad7a1 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 d9041dad7206..dac067bdc4cb 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 59eaecb4ed71..06774dc75c19 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 a00918bacb40..702ac7fa1c11 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 72a00e1bc202..f94e05a05f15 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 164c76e08640..9f6ff6b7d57a 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 445a541abf63..d5a70b39cc6a 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 @@ 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 @@ }, }, + '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 @@ }, }, + '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 @@ 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 fc5a27c81f7c..900237acde45 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3219,6 +3219,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariableInfo VariableSetKind VariableSetStmt VariableShowStmt From 24876abadbda4dfac26ba8354c5775910ae46c81 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 420a4d9ff112..2655fa5e7ce2 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5396,6 +5396,17 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. + + + In an query the session variable can be used only inside + variable fence. 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. + +SELECT VARIABLE(current_user_id); + + diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 800f37dc2ea0..b20d2074382d 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 34b6410d6a26..fcadcd9bc3ff 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 ede838cd40c4..c761fde6acb3 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 3b392b084ad6..c3897aafd211 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 e831142efe22..892ef74e0905 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -531,7 +531,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type 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 opt_column_and_period_list %type rowsfrom_item rowsfrom_list opt_col_def_list %type 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 '*' '/' '%' @@ -15769,6 +15769,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 @@ -17151,6 +17164,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 32d6ae918caa..8b32f74de239 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 51d7703eff7e..244efcddf329 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 905c975d83b5..8b5240cd54bb 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 5398679cce22..eec110fe054c 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 1c4031eea23c..b9b0ac554759 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 d12c3b957b7a..5aa86b1db71e 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 6dde01f2c7b7..99947cadee89 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 1b4436f2ff6d..5f3a1a2d1be2 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 f7d07c845425..84e886940d81 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 03c4c58580e9..5926a854d12c 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 d19425b7a71a..96857874ffe2 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 900237acde45..02d9d76d1e45 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3211,6 +3211,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState From a080430d45b48efff53d65182c26fb21ded1d638 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 c37fd5da50b1..97cd13957bbb 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1717,8 +1717,9 @@ 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. For more information, see . diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c index f641e00c1acc..dbc054795bbb 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 49f36ac68857..9f5c6e30fbd6 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 02d9d76d1e45..63e7f6ca1edf 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2663,6 +2663,8 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan From b159005a4cff1d893621ff372abdcf7ecb4c8db8 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 1ce9abf86f52..683dede6adce 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 SubPlan. + + + + Plan nodes that use a session variable. + + diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 1d62e63d4f7f..eb91b46b1288 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 c4fd646b999c..cc03b0311a47 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 ccdc9bc264ab..a12a3c6094cb 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 481d8011791b..2355a7ce9221 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 81d768ff2a26..55c2ec50c950 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 6661d2c6b739..ab1f2af13e5f 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 0fe63c6bb830..d26e744e2963 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 30d889b54c53..75a863dad935 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 c4393a943211..0aa52cbc4c56 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 00addf159925..fb81ceb375fd 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 */ From 767655e5af1051579f91e92fbd98c4067c74b2db Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 f1569879b529..0457a729537a 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 27c9eec697b1..4c5b101c8ee4 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 ab1f2af13e5f..1d20f3382b0b 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 18ae8f0d4bb8..b6048d969009 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 000000000000..3e21059acc2f --- /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: +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: +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 d8000da3f865..860c526edbee 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 000000000000..b2870dde9e94 --- /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 63e7f6ca1edf..42f1e2b018e8 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2723,6 +2723,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData From ff45aa85bb01da60a9974d88e37bd86f01e5d257 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 11118d0ce025..71248a34f264 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 2cea41f87711..491092fcc4c7 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 000000000000..aadd446f2772 --- /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 b620766c9388..b2f764b657fc 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 000000000000..db44d8b94c6e --- /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 00c092e3d7c0..6ce3ea0e617d 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 42f1e2b018e8..82eafa9303cf 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2665,6 +2665,7 @@ STRLEN SV SVariableData SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan From 4b3763867df38ec66db78dc935cde4a703a3cbcd Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 2655fa5e7ce2..bdf00b355085 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5403,10 +5403,39 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; session variable identifier, and can be used only for session variable identifier. The special syntax for accessing session variables removes risk of collisions between variable identifiers and column names. + + + + The value of a session variable is set with the SQL statement + LET. The value of a session variable can be retrieved + with the SQL statement SELECT. +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); + + + or + + +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); + + + The value of a session variable is local to the current session. Retrieving + a variable's value returns a NULL, unless its value has + been set to something else in the current session using the + LET 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). + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b6..cc3bd5ab5403 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. + diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e4..221a699469b6 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; + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e4729..43000ce004d8 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 ] nameExamples - Create an date session variable var1: + Create a session variable var1 of data type date: CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; + diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b0..67988b5fcd82 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; + diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 000000000000..00f9bea91fe3 --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ + + + + + LET + + + + session variable + changing + + + + LET + 7 + SQL - Language Statements + + + + LET + change a session variable's value + + + + +LET session_variable = sql_expression + + + + + Description + + + The LET command assigns a value to the specified session + variable. + + + + + + Parameters + + + + session_variable + + + The name of the session variable. + + + + + + sql_expression + + + 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. + + + + + + + + + Examples + +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); + + + + + Compatibility + + + The LET is a PostgreSQL + extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946cd..13e4adc5df3e 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 dbc054795bbb..9bdc92bfd308 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 4c5b101c8ee4..76f478ff580e 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 c761fde6acb3..0d5461cbd6b7 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 cc03b0311a47..8dccd50737aa 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 a12a3c6094cb..0cbe09834c5d 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 c3897aafd211..4038876497d6 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 892ef74e0905..79105b003c0b 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 LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1095,6 +1095,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12982,6 +12983,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: @@ -18078,6 +18111,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18693,6 +18727,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 3254c83cc6cd..ddf7905654c6 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 8b32f74de239..ff16a3bc3cc2 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 778d69c6f3c2..13616c9b3c27 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 1a0e11ba7017..ab0400c1f7af 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -235,6 +235,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1057,6 +1058,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2198,6 +2204,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2396,6 +2406,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3281,6 +3295,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 1d20f3382b0b..c8e91be14827 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 094e65db8496..fe020a9b3816 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 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 9f5c6e30fbd6..2ebe84777894 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 99947cadee89..834b44a61026 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 75a863dad935..50d4a20be777 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 0aa52cbc4c56..cf863c43d8c8 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 5f3a1a2d1be2..9107891fa9d0 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 7e7d262b1bb0..1ccaacc39ad7 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 84e886940d81..026743b7337e 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 ea86954dded7..22082c30008b 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 3e21059acc2f..67f78a548dc9 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 b2870dde9e94..2c8d6c3a497f 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 82eafa9303cf..1ceeb23e909d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1548,6 +1548,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey From 560a9ef282d77c9066fd64c0ae1da10c98c87f66 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 9bdc92bfd308..3bafa7015093 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 8de4d9cfa760..9fb7be987bc5 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' }, ] From c2ff31e03b0b610ba80210f55f83e9684042a87a Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 bf44c523cac6..61b967f9c9b7 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES } @@ -66,6 +66,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } + + VARIABLES + + + Resets the value of all session variables. If a variable + is later reused, it is re-initialized to NULL. + + + + TEMPORARY or TEMP @@ -93,6 +103,7 @@ SELECT pg_advisory_unlock_all(); DISCARD PLANS; DISCARD TEMP; DISCARD SEQUENCES; +DISCARD VARIABLES; diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a528..5904a6c49171 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 3bafa7015093..c7f6424b76bb 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 79105b003c0b..c57c873c956b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2146,7 +2146,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 ab0400c1f7af..7a9271d0639d 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -2953,6 +2953,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = CMDTAG_DISCARD_SEQUENCES; break; + case DISCARD_VARIABLES: + tag = CMDTAG_DISCARD_VARIABLES; + break; default: tag = CMDTAG_UNKNOWN; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index fe020a9b3816..a4597abad05d 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 2ebe84777894..ac36dfcc19b9 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 834b44a61026..b09eb3351bc6 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 22082c30008b..bef0ac253314 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 67f78a548dc9..dfbe7900c1c5 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 2c8d6c3a497f..b0aeeb865c21 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; From ce81fbf673c11c0352754da80aec761a7bf7066e Mon Sep 17 00:00:00 2001 From: "okbob@github.com" 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 d8ede4fa8c86..c9411443c6d5 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 c7f6424b76bb..bb063bb7bbc6 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 ac36dfcc19b9..c06e1faf02c8 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 000000000000..b69dac2df7a5 --- /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 5afae33d3703..3068ae401dbc 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 000000000000..72629321d904 --- /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 9c7595e9a417..3aef7665edc9 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 f844469ecb1f..a616ecdc8ef6 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; From b6408ba423fb6aea652e40fb09bf6759be060e19 Mon Sep 17 00:00:00 2001 From: Laurenz Albe 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 63cb96fae3ef..bbcae27d422d 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 000000000000..ec5ffcb42b25 --- /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 33c49ac25d94..1d01d1c26291 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 000000000000..61d46a792ec9 --- /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;