diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index cea28c00f8a8..013610ae0628 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5693,6 +5693,58 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; + + Session Variables + + + Session variables + + + + session variable + + + + Session variables are temporary database objects that can hold a value. + A session variable can be created by the CREATE VARIABLE + command and can only be accessed by its owner. The value of a session + variable is stored in session memory and is private to each session. It is + automatically released when the session ends. + + + + In a query, a session variable can only be referenced using the special + VARIABLE(varname) syntax. This avoids any risk of + collision between variable names and column names. + + + + You set the value of a session variable with the LET + statement and retrieve it with SELECT: + +CREATE TEMP VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); + + + or + + +CREATE TEMP VARIABLE current_user_id AS integer; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); +SELECT VARIABLE(current_user_id); + + + + + By default, retrieving a session variable returns + NULL unless it has been set in the current session + using the LET command. Session variables are not + transactional: changes to their values persist even if the transaction + is rolled back, similar to variables in procedural languages + + + Other Database Objects diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index a76cf5c383fc..64d5fd402191 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1726,6 +1726,21 @@ + + Session variable + + + A temporal database object that holds a value in session memory. This + value is private to each session and is released when the session ends. + The default value of the session variable is null. Read or write access + to session variables is allowed only to owner (creator). + + + For more information, see . + + + + Shared memory diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index af43484703eb..843e2c3f663f 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -524,6 +524,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated SubPlan. + + + + Usage of a session variable. + + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index e167406c7449..cd3faa667f06 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -99,6 +99,7 @@ Complete list of usable sgml source files in this directory. + @@ -147,6 +148,7 @@ Complete list of usable sgml source files in this directory. + @@ -155,6 +157,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 000000000000..def368fc2374 --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,146 @@ + + + + + CREATE VARIABLE + + + + session variable + defining + + + + CREATE VARIABLE + 7 + SQL - Language Statements + + + + CREATE VARIABLE + define a session variable + + + + +CREATE { TEMP | TEMPORARY } VARIABLE [ IF NOT EXISTS ] name [ AS ] data_type + + + + Description + + + The CREATE VARIABLE command creates a session + variable. Currently only temporary session variables are supported, + and then the keyword TEMPORARY is required. + + + + 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 cannot be used in views or in SQL functions using + SQL-conforming style syntax. + + + + + 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 of the session variable. + + + + + + data_type + + + The name, optionally schema-qualified, of the data type of the session + variable. Only buildin scalar data types are allowed. Arrays or composite + types are not allowed. + + + + + + + + + Notes + + + Use the DROP VARIABLE command to remove a session + variable. + + + + + Examples + + + Create an date session variable var1: + +CREATE TEMPORARY VARIABLE var1 AS date; +LET var1 = current_date; +SELECT VARIABLE(var1); + + + + + + + Compatibility + + + The CREATE VARIABLE command is a + PostgreSQL extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index bf44c523cac6..2700f7b7cd07 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -70,7 +70,8 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } TEMPORARY or TEMP - Drops all temporary tables created in the current session. + Drops all temporary tables and temporary session variables created in + the current session. diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 000000000000..5de6a737493d --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,95 @@ + + + + + DROP VARIABLE + + + + session variable + removing + + + + DROP VARIABLE + 7 + SQL - Language Statements + + + + DROP VARIABLE + remove a session variable + + + + +DROP VARIABLE [ IF EXISTS ] name + + + + + 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 of a session variable. + + + + + + + + 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/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 000000000000..33ee42d3f204 --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,95 @@ + + + + + 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 TEMPORARY 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 2cf02c37b17b..6fcd7a81321a 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -127,6 +127,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +176,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; @@ -183,6 +185,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b69d452551ac..f0f67b72830c 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -39,6 +39,7 @@ #include "commands/async.h" #include "commands/tablecmds.h" #include "commands/trigger.h" +#include "commands/session_variable.h" #include "common/pg_prng.h" #include "executor/spi.h" #include "libpq/be-fsstubs.h" @@ -2333,6 +2334,9 @@ CommitTransaction(void) /* close large objects before lower-level cleanup */ AtEOXact_LargeObject(true); + /* remove stacked session variables */ + AtPreEOXact_SessionVariables(true); + /* * Insert notifications sent by NOTIFY commands into the queue. This * should be late in the pre-commit sequence to minimize time spent @@ -2937,6 +2941,7 @@ AbortTransaction(void) AtAbort_Portals(); smgrDoPendingSyncs(false, is_parallel_worker); AtEOXact_LargeObject(false); + AtPreEOXact_SessionVariables(false); AtAbort_Notify(); AtEOXact_RelationMap(false, is_parallel_worker); AtAbort_Twophase(); @@ -5203,6 +5208,8 @@ CommitSubTransaction(void) AtEOSubXact_SPI(true, s->subTransactionId); AtEOSubXact_on_commit_actions(true, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(true, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(true, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(true, s->subTransactionId, @@ -5372,6 +5379,8 @@ AbortSubTransaction(void) AtEOSubXact_SPI(false, s->subTransactionId); AtEOSubXact_on_commit_actions(false, s->subTransactionId, s->parent->subTransactionId); + AtEOSubXact_SessionVariables(false, s->subTransactionId, + s->parent->subTransactionId); AtEOSubXact_Namespace(false, s->subTransactionId, s->parent->subTransactionId); AtEOSubXact_Files(false, s->subTransactionId, diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 7489bbd5fb34..f7346b3aee17 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1951,6 +1951,16 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* + * catalog less session variable variable cannot be used in persistent + * catalog based object. + */ + if (param->paramkind == PARAM_VARIABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable \"%s\" cannot be referenced in a catalog object", + param->paramvarname))); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 64cb6278409f..d42ed8952a2b 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -54,6 +54,7 @@ OBJS = \ seclabel.o \ sequence.o \ sequence_xlog.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a528..de322c35c517 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -19,6 +19,7 @@ #include "commands/discard.h" #include "commands/prepare.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "utils/guc.h" #include "utils/portal.h" @@ -46,6 +47,7 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) case DISCARD_TEMP: ResetTempTableNamespace(); + ResetSessionVariables(); break; default: @@ -75,4 +77,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSessionVariables(); } diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63b..9524f867857a 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -22,6 +22,7 @@ #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "commands/defrem.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "parser/parse_type.h" #include "utils/acl.h" diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index 5fc35826b1cc..1eeb18fd960a 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -42,6 +42,7 @@ backend_sources += files( 'seclabel.c', 'sequence.c', 'sequence_xlog.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 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/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 000000000000..4a311d2498bb --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,761 @@ +/*------------------------------------------------------------------------- + * + * 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 "access/xact.h" +#include "catalog/pg_language.h" +#include "commands/session_variable.h" +#include "executor/executor.h" +#include "executor/svariableReceiver.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/snapmgr.h" + +/* + * The session variables are stored in the backend's private memory (data, + * metadata) in the dedicated memory context SVariableMemoryContext in binary + * format. They are stored in the "sessionvars" hash table, whose key is the + * name of the variable. + * + * Only owner (creator) can access the session variables. Because there is + * not catalog support, there is not possibility to track dependecies, and + * then only buildin types. + */ +typedef struct SVariableData +{ + NameData varname; + + Oid varowner; + Oid vartype; + int32 vartypmod; + Oid varcollation; + + bool isnull; + Datum value; + + int16 typlen; + bool typbyval; + + struct SVariableData *prev; + bool stacked; + LocalTransactionId created_lxid; + LocalTransactionId dropped_lxid; + SubTransactionId created_subid; + SubTransactionId dropped_subid; +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * When we to remove committed dropped variables or uncommitted + * created variables from sessionvars tab. created_or_dropped_lxid + * is transaction id of transaction when some of DROP or CREATE variable + * was executed. + */ +static LocalTransactionId created_or_dropped_lxid = InvalidLocalTransactionId; + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + vars_ctl.keysize = NAMEDATALEN; + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); +} + +/* + * Returns entry of session variable specified by name + */ +static SVariable +search_variable(char *varname, bool missing_ok) +{ + SVariable svar; + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, varname, + HASH_FIND, NULL); + + /* Session variable can be dropped inside current transaction */ + if (svar && svar->dropped_lxid != InvalidLocalTransactionId) + { + Assert(created_or_dropped_lxid == MyProc->vxid.lxid); + Assert(svar->dropped_lxid == MyProc->vxid.lxid); + svar = NULL; + } + + if (!svar && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + varname))); + + return svar; +} + +/* + * Returns the type, typmod and collid of the given session variable. + * + * Raises an error when the variable doesn't exists and *error is null. + */ +void +get_session_variable_type_typmod_collid(char *varname, + Oid *typid, + int32 *typmod, + Oid *collid) +{ + SVariable svar; + + svar = search_variable(varname, false); + + /* only owner can set content of variable */ + *typid = svar->vartype; + *typmod = svar->vartypmod; + *collid = svar->varcollation; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). + */ +Datum +GetSessionVariableWithTypecheck(char *varname, + Oid typid, int32 typmod, + bool *isnull) +{ + SVariable svar; + Datum result; + + svar = search_variable(varname, false); + + if (svar->vartype != typid || svar->vartypmod != typmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("session variable %s is not of a type %s but type %s", + varname, + format_type_with_typemod(typid, typmod), + format_type_with_typemod(svar->vartype, svar->vartypmod)))); + + /* only owner can get content of variable */ + if (svar->varowner != GetUserId() && !superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied for session variable %s", + varname))); + + if (!svar->isnull) + result = datumCopy(svar->value, svar->typbyval, svar->typlen); + else + result = (Datum) 0; + + *isnull = svar->isnull; + + return result; +} + +/* + * Store the given value in a session variable in the cache. + */ +void +SetSessionVariableWithTypecheck(char *varname, + Oid typid, int32 typmod, + Datum value, bool isnull) +{ + SVariable svar; + + svar = search_variable(varname, false); + + if (svar->vartype != typid || svar->vartypmod != typmod) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("session variable %s is not of a type %s but type %s", + varname, + format_type_with_typemod(typid, typmod), + format_type_with_typemod(svar->vartype, svar->vartypmod)))); + + /* only owner can set content of variable */ + if (svar->varowner != GetUserId() && !superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied for session variable %s", + varname))); + + if (!svar->typbyval) + { + if (!isnull) + { + MemoryContext oldcxt; + + /* + * Do copy of value in session variables context. This operation + * can fail, so do it before releasing the old content. + */ + oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + value = datumCopy(value, svar->typbyval, svar->typlen); + MemoryContextSwitchTo(oldcxt); + } + + if (!svar->isnull) + pfree(DatumGetPointer(svar->value)); + } + + svar->value = value; + svar->isnull = isnull; +} + +/* + * Creates a new variable - does new entry in sessionvars + * + * Used by CREATE VARIABLE command + */ +void +CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt) +{ + Oid typeid; + int32 typmod; + Oid typcollation; + Oid varowner = GetUserId(); + SVariable svar; + SVariable prev_svar = NULL; + bool found; + int16 typlen; + bool typbyval; + + /* + * Current implementation is not catalog based, but we expect catalog + * based implementation for future, so we force same limits. + */ + PreventCommandIfReadOnly("CREATE VARIABLE"); + PreventCommandIfParallelMode("CREATE VARIABLE"); + PreventCommandDuringRecovery("CREATE VARIABLE"); + + typenameTypeIdAndMod(pstate, stmt->typeName, &typeid, &typmod); + + if (get_typtype(typeid) != TYPTYPE_BASE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s is not a base type", + format_type_be(typeid)))); + + if (OidIsValid(get_element_type(typeid))) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s type is an array", + format_type_be(typeid)))); + + /* allow only buildin types */ + if (typeid >= FirstUnpinnedObjectId) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable cannot have a user-defined type"), + errdetail("Session variables that make use of user-defined types are not yet supported.")); + + get_typlenbyval(typeid, &typlen, &typbyval); + typcollation = get_typcollation(typeid); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = hash_search(sessionvars, stmt->name, + HASH_ENTER, &found); + + if (found) + { + if (svar->dropped_lxid == InvalidLocalTransactionId) + { + if (stmt->if_not_exists) + { + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists, skipping", + stmt->name))); + return; + } + else + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("session variable \"%s\" already exists", + stmt->name))); + } + else + { + MemoryContext oldcxt; + + Assert(created_or_dropped_lxid == MyProc->vxid.lxid); + Assert(svar->dropped_lxid == MyProc->vxid.lxid); + + oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + prev_svar = palloc_object(SVariableData); + memcpy(prev_svar, svar, sizeof(SVariableData)); + prev_svar->stacked = true; + memset(svar, 0, sizeof(SVariableData)); + + MemoryContextSwitchTo(oldcxt); + } + } + + namestrcpy(&svar->varname, stmt->name); + svar->vartype = typeid; + svar->vartypmod = typmod; + svar->varcollation = typcollation; + svar->varowner = varowner; + svar->typlen = typlen; + svar->typbyval = typbyval; + + svar->value = (Datum) 0; + svar->isnull = true; + + svar->prev = prev_svar; + svar->stacked = false; + svar->dropped_lxid = InvalidLocalTransactionId; + svar->created_lxid = MyProc->vxid.lxid; + svar->dropped_subid = InvalidSubTransactionId; + svar->created_subid = GetCurrentSubTransactionId(); + created_or_dropped_lxid = MyProc->vxid.lxid; +} + +/* + * Drop variable by name + */ +void +DropVariableByName(DropSessionVarStmt *stmt) +{ + SVariable svar; + + /* + * Current implementation is not catalog based, but we expect catalog + * based implementation for future, so we force same limits. + */ + PreventCommandIfReadOnly("DROP VARIABLE"); + PreventCommandIfParallelMode("DROP VARIABLE"); + PreventCommandDuringRecovery("DROP VARIABLE"); + + svar = search_variable(stmt->name, stmt->missing_ok); + if (!svar) + { + ereport(NOTICE, + (errmsg("session variable \"%s\" does not exists, skipping", + stmt->name))); + return; + } + + /* only owner can get content of variable */ + if (svar->varowner != GetUserId() && !superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be owner of session variable %s", + stmt->name))); + + svar->dropped_lxid = MyProc->vxid.lxid; + svar->dropped_subid = GetCurrentSubTransactionId(); + created_or_dropped_lxid = MyProc->vxid.lxid; +} + +static void +free_svar_value(SVariable svar) +{ + if (!svar->isnull && !svar->typbyval) + pfree(DatumGetPointer(svar->value)); +} + +static void +free_stacked_svars(SVariable svar) +{ + while (svar) + { + SVariable current = svar; + + free_svar_value(current); + svar = current->prev; + pfree(current); + } +} + +/* + * remove dropped committed entries or created uncommitted entries + * from hash table. + */ +void +AtPreEOXact_SessionVariables(bool isCommit) +{ + if (created_or_dropped_lxid != InvalidLocalTransactionId) + { + HASH_SEQ_STATUS status; + SVariable svar; + + Assert(created_or_dropped_lxid == MyProc->vxid.lxid); + Assert(sessionvars); + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if ((svar->dropped_lxid != InvalidLocalTransactionId) || + (svar->created_lxid != InvalidLocalTransactionId)) + { + Assert((svar->dropped_lxid == InvalidLocalTransactionId) || + (svar->dropped_lxid == MyProc->vxid.lxid)); + + Assert((svar->created_lxid == InvalidLocalTransactionId) || + (svar->created_lxid == MyProc->vxid.lxid)); + + if (isCommit) + { + if (svar->dropped_lxid == MyProc->vxid.lxid) + { + free_stacked_svars(svar->prev); + free_svar_value(svar); + + (void) hash_search(sessionvars, + NameStr(svar->varname), + HASH_REMOVE, + NULL); + svar = NULL; + } + else + { + free_stacked_svars(svar->prev); + svar->prev = NULL; + svar->created_lxid = InvalidLocalTransactionId; + svar->created_subid = InvalidSubTransactionId; + } + } + else + { + SVariable iter; + + /* + * We have to search value the oldest svar in the stack. If it is just dropped, + * then we revert dropped flag. If it is created in current transaction, then + * we remove this svar too. + */ + iter = svar; + while (iter->prev) + { + SVariable current = iter; + + free_svar_value(current); + + iter = current->prev; + + if (current->stacked) + pfree(current); + } + + if (iter->created_lxid == MyProc->vxid.lxid) + { + free_svar_value(iter); + if (iter->stacked) + pfree(iter); + + (void) hash_search(sessionvars, + NameStr(svar->varname), + HASH_REMOVE, + NULL); + } + else + { + if (iter->stacked) + { + memcpy(svar, iter, sizeof(SVariableData)); + svar->stacked = false; + pfree(iter); + } + + /* revert dropped flag */ + svar->dropped_lxid = InvalidLocalTransactionId; + svar->dropped_subid = InvalidSubTransactionId; + } + } + } + } + + created_or_dropped_lxid = InvalidLocalTransactionId; + } +} + +/* + * Post-subcommit or post-subabort cleanup + * + * During subabort, we can immediately remove entries created during this + * subtransaction. During subcommit, just transfer entries marked during + * this subtransaction as being the parent's responsibility. + */ +void +AtEOSubXact_SessionVariables(bool isCommit, + SubTransactionId mySubid, + SubTransactionId parentSubid) +{ + if (created_or_dropped_lxid != InvalidLocalTransactionId) + { + HASH_SEQ_STATUS status; + SVariable svar; + + Assert(created_or_dropped_lxid == MyProc->vxid.lxid); + Assert(sessionvars); + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if ((svar->dropped_lxid != InvalidLocalTransactionId) || + (svar->created_lxid != InvalidLocalTransactionId)) + { + if (!isCommit) + { + SVariable iterator = svar; + SVariable last = NULL; + SVariable first = NULL; + + /* remove entries or flags by current subtransactions */ + while (iterator) + { + SVariable current = iterator; + + iterator = current->prev; + + if (current->dropped_subid == mySubid) + { + current->dropped_lxid = InvalidLocalTransactionId; + current->dropped_subid = InvalidSubTransactionId; + } + + if (current->created_subid == mySubid) + { + free_svar_value(current); + if (current->stacked) + pfree(current); + } + else + { + /* remember first not deleted svar */ + if (first == NULL) + first = current; + + if (last) + last->prev = current; + + last = current; + } + } + + /* Some svars was removed - set hashtab entry or remove it */ + if (!first) + { + /* we have to remove entry from hash table */ + (void) hash_search(sessionvars, + NameStr(svar->varname), + HASH_REMOVE, + NULL); + } + else if (first->stacked) + { + memcpy(svar, first, sizeof(SVariableData)); + svar->stacked = false; + pfree(first); + } + } + else + { + SVariable iterator = svar; + + /* transfer responsibility to parent */ + while (iterator) + { + if (iterator->dropped_subid == mySubid) + iterator->dropped_subid = parentSubid; + if (iterator->created_subid == mySubid) + iterator->created_subid = parentSubid; + + iterator = iterator->prev; + } + } + } + } + } +} + +/* + * 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; + PlannedStmt *plan; + QueryDesc *queryDesc; + char *varname = query->resultVariable; + SVariable svar; + + svar = search_variable(varname, false); + + /* only owner can set content of variable */ + if (svar->varowner != GetUserId() && !superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied for session variable %s", + varname))); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varname); + + /* 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(); +} + +/* + * This is used by the DISCARD TEMP. + */ +void +ResetSessionVariables(void) +{ + /* mark all session variables as dropped */ + if (sessionvars) + { + HASH_SEQ_STATUS status; + SVariable svar; + bool found = false; + + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + svar->dropped_lxid = MyProc->vxid.lxid; + found = true; + } + + if (found) + created_or_dropped_lxid = MyProc->vxid.lxid; + } +} + +/* + * pg_get_temporary_session_variables_names + * + * Returns list of temporary session variables. It is used by psql's + * tab complete for DROP VARIABLE and LET commands. + */ +Datum +pg_get_temporary_session_variables_names(PG_FUNCTION_ARGS) +{ + InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC); + + 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[1]; + bool nulls[1]; + + values[0] = CStringGetTextDatum((NameStr(svar->varname))); + nulls[0] = false; + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + } + + return (Datum) 0; +} 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/execExpr.c b/src/backend/executor/execExpr.c index c35744b105e5..a675ca07cd9a 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 797d8b1ca1cb..0485bfd1487b 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -44,6 +44,7 @@ #include "catalog/namespace.h" #include "catalog/partition.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -196,6 +197,54 @@ 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 = palloc_array(SessionVariableValue, + nSessionVariables); + + /* fill the array */ + foreach_node(Param, param, queryDesc->plannedstmt->sessionVariables) + { + estate->es_session_variables[i].value = + GetSessionVariableWithTypecheck(param->paramvarname, + param->paramtype, + param->paramtypmod, + &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/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..b2709e9211b0 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,149 @@ +/*------------------------------------------------------------------------- + * + * 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 "commands/session_variable.h" +#include "executor/svariableReceiver.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; + char *varname; + Oid typid; + int32 typmod; + 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; + Form_pg_attribute attr; + + Assert(myState->pub.mydest == DestVariable); + Assert(typeinfo->natts == 1); + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + + myState->typid = attr->atttypid; + myState->typmod = attr->atttypmod; + + 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"))); + + SetSessionVariableWithTypecheck(myState->varname, + myState->typid, myState->typmod, + 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(((SVariableState *) self)->varname); + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(char *varname) +{ + 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->varname = pstrdup(varname); + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 024a2b2fd841..6966c3dbec80 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; @@ -4371,6 +4374,14 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; @@ -4701,6 +4712,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/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 8b22c30559b2..81613e7760e1 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -374,6 +374,8 @@ 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; + glob->resultVariable = parse->resultVariable; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -617,6 +619,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 +810,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 cd7ea1e6b587..b8b9444051d1 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,8 @@ 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); /***************************************************************************** @@ -1341,6 +1343,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 @@ -2141,6 +2187,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 +2209,43 @@ 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; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_node(Param, paramvar, root->glob->sessionVariables) + { + if (strcmp(paramvar->paramvarname, p->paramvarname) == 0) + { + p->paramid = paramvar->paramid; + + return (Node *) p; + } + + n += 1; + } + + p->paramid = n; + + /* + * Because session variables are catalogless, we cannot to use plan + * invalidation. Then we need to check type, typmod, collid any time, + * when we load values of session variables to parameter's buffer. + * For this purpose it is more easy to save complete Param node. + */ + root->glob->sessionVariables = lappend(root->glob->sessionVariables, p); + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2220,7 +2307,9 @@ 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 + * of session variables in the root->glob->sessionVariables list. * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2242,7 +2331,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); } diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index c3b726e93e7d..12301440d822 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1649,6 +1649,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 ddafc21c819a..7e80d763cbdd 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" @@ -947,6 +948,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)) { @@ -2405,6 +2413,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 * @@ -2531,6 +2540,29 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, &typLen, &typByVal); + + pval = GetSessionVariableWithTypecheck(param->paramvarname, + param->paramtype, + param->paramtypmod, + &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 @@ -4903,7 +4935,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 92be345d9a89..36b037f52487 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -53,15 +53,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 */ @@ -85,7 +88,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); @@ -95,6 +98,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 @@ -342,6 +347,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: @@ -421,6 +427,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -482,6 +493,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_ExplainStmt: case T_CreateTableAsStmt: case T_CallStmt: + case T_LetStmt: result = true; break; @@ -547,6 +559,7 @@ query_requires_rewrite_plan(Query *query) case T_ExplainStmt: case T_CreateTableAsStmt: case T_CallStmt: + case T_LetStmt: result = true; break; default: @@ -619,6 +632,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); @@ -1044,6 +1058,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); @@ -1388,7 +1403,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. @@ -1441,8 +1456,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); @@ -1527,6 +1542,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) { @@ -1753,6 +1769,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); @@ -2004,6 +2021,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) { @@ -2476,6 +2494,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); @@ -2543,6 +2562,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); @@ -2834,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 @@ -2859,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; @@ -2905,7 +2930,7 @@ transformPLAssignStmtTarget(ParseState *pstate, List *tlist, tle->expr = (Expr *) transformAssignmentIndirection(pstate, target, - stmt->name, + target_name, false, targettype, targettypmod, @@ -2913,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))) { @@ -2939,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 */ @@ -2948,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."), @@ -3316,6 +3341,59 @@ 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; + VariableFence *vf; + SelectStmtPassthrough passthru; + Param *paramvar; + + /* gram allows only SELECT */ + Assert(IsA(stmt->query, SelectStmt)); + + /* Use implicit VariableFence for forcing session variables */ + vf = makeNode(VariableFence); + vf->varname = stmt->target; + vf->location = stmt->location; + + target = transformExpr(pstate, (Node *) vf, EXPR_KIND_LET_TARGET); + + paramvar = castNode(Param, target); + + Assert(paramvar->paramkind == PARAM_VARIABLE); + + /* Set up passthrough data for transformAssignTarget */ + passthru.stmt = (Node *) stmt; + passthru.target = (Node *) paramvar; + passthru.target_name = paramvar->paramvarname; + passthru.indirection = NIL; + 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->paramvarname; + 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 28f4e11e30ff..426a7f1aea41 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -291,18 +291,18 @@ 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 DropOpClassStmt DropOpFamilyStmt DropStmt - DropCastStmt DropRoleStmt + DropCastStmt DropRoleStmt DropSessionVarStmt DropdbStmt DropTableSpaceStmt 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 @@ -532,7 +532,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 @@ -750,7 +750,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LET LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED LSN_P MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -792,8 +792,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UESCAPE UNBOUNDED UNCONDITIONAL UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARIABLE + VARYING VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -890,7 +890,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 '*' '/' '%' @@ -1059,6 +1059,7 @@ stmt: | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt + | CreateSessionVarStmt | CreateSeqStmt | CreateStmt | CreateSubscriptionStmt @@ -1086,6 +1087,7 @@ stmt: | DropTableSpaceStmt | DropTransformStmt | DropRoleStmt + | DropSessionVarStmt | DropUserMappingStmt | DropdbStmt | ExecuteStmt @@ -1096,6 +1098,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -5388,6 +5391,74 @@ create_extension_opt_item: } ; +/***************************************************************************** + * + * QUERY : + * CREATE { TEMP | TEMPORARY } VARIABLE [IF NOT EXISTS ] varname [AS] type + * + *****************************************************************************/ + +CreateSessionVarStmt: + CREATE OptTemp VARIABLE ColId opt_as Typename + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + + if ($2 != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only temporal session variables are supported"), + parser_errposition(@2))); + + n->name = $4; + n->typeName = $6; + n->if_not_exists = false; + $$ = (Node *) n; + } + | CREATE OptTemp VARIABLE IF_P NOT EXISTS ColId opt_as Typename + { + CreateSessionVarStmt *n = makeNode(CreateSessionVarStmt); + + if ($2 != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only temporal session variables are supported"), + parser_errposition(@2))); + + n->name = $7; + n->typeName = $9; + n->if_not_exists = true; + $$ = (Node *) n; + } + + ; + +/***************************************************************************** + * + * QUERY : + * DROP VARIABLE [ IF EXISTS ] varname + * + *****************************************************************************/ + +DropSessionVarStmt: + DROP VARIABLE ColId + { + DropSessionVarStmt *n = makeNode(DropSessionVarStmt); + + n->name = $3; + n->missing_ok = false; + $$ = (Node *) n; + } + | DROP VARIABLE IF_P EXISTS ColId + { + DropSessionVarStmt *n = makeNode(DropSessionVarStmt); + + n->name = $5; + n->missing_ok = true; + $$ = (Node *) n; + } + + ; + /***************************************************************************** * * ALTER EXTENSION name UPDATE [ TO version ] @@ -12941,6 +13012,37 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = $2; + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -15728,6 +15830,8 @@ c_expr: columnref { $$ = $1; } else $$ = $2; } + | variable_fence + { $$ = $1; } | case_expr { $$ = $1; } | func_expr @@ -17130,6 +17234,17 @@ case_arg: a_expr { $$ = $1; } | /*EMPTY*/ { $$ = NULL; } ; +variable_fence: + VARIABLE '(' ColId ')' + { + VariableFence *vf = makeNode(VariableFence); + + vf->varname = $3; + vf->location = @3; + $$ = (Node *) vf; + } + ; + columnref: ColId { $$ = makeColumnRef($1, NIL, @1, yyscanner); @@ -18033,6 +18148,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18208,6 +18324,7 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE | VARYING | VERSION_P | VIEW @@ -18650,6 +18767,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN @@ -18868,6 +18986,7 @@ bare_label_keyword: | VALUE_P | VALUES | VARCHAR + | VARIABLE | VARIADIC | VERBOSE | VERSION_P diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index b8340557b345..a9bd4e681549 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -584,6 +584,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 @@ -1023,6 +1027,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 6b8fa15fca33..23a0c8752bc1 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -18,6 +18,7 @@ #include "access/htup_details.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.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, @@ -371,6 +373,10 @@ transformExprRecurse(ParseState *pstate, Node *expr) result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr); break; + case T_VariableFence: + result = transformVariableFence(pstate, (VariableFence *) expr); + break; + default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); @@ -585,6 +591,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 @@ -904,6 +913,120 @@ 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: + case EXPR_KIND_LET_TARGET: + 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) +{ + Param *param; + Oid typid; + int32 typmod; + Oid collid; + + /* 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))); + + get_session_variable_type_typmod_collid(vf->varname, + &typid, &typmod, &collid); + + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarname = pstrdup(vf->varname); + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + return (Node *) param; +} + /* Test whether an a_expr is a plain NULL constant or not */ static bool exprIsNullConstant(Node *arg) @@ -1861,6 +1984,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 @@ -3220,6 +3346,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/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..2f42627f0096 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 = ((VariableFence *) node)->varname; + return 2; + } + break; default: break; } diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c9388..0c1eeeb22a68 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(NULL); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index d18a3a60a467..fd911f5234f9 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -48,6 +48,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -183,6 +184,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateRangeStmt: case T_CreateRoleStmt: case T_CreateSchemaStmt: + case T_CreateSessionVarStmt: case T_CreateSeqStmt: case T_CreateStatsStmt: case T_CreateStmt: @@ -201,6 +203,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_DropTableSpaceStmt: case T_DropUserMappingStmt: case T_DropdbStmt: + case T_DropSessionVarStmt: case T_GrantRoleStmt: case T_GrantStmt: case T_ImportForeignSchemaStmt: @@ -234,6 +237,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1063,6 +1067,20 @@ standard_ProcessUtility(PlannedStmt *pstmt, } break; + case T_CreateSessionVarStmt: + CreateVariable(pstate, (CreateSessionVarStmt *) parsetree); + break; + + case T_DropSessionVarStmt: + /* No event triggers for catalog less session variables */ + DropVariableByName((DropSessionVarStmt *) parsetree); + 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, @@ -1388,6 +1406,7 @@ ProcessUtilitySlow(ParseState *pstate, } break; + /* * ************* object creation / destruction ************** */ @@ -2206,6 +2225,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2401,6 +2424,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: { @@ -3235,6 +3262,14 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreateSessionVarStmt: + tag = CMDTAG_CREATE_VARIABLE; + break; + + case T_DropSessionVarStmt: + tag = CMDTAG_DROP_VARIABLE; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -3283,6 +3318,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; @@ -3773,6 +3809,11 @@ GetCommandLogLevel(Node *parsetree) } break; + case T_CreateSessionVarStmt: + case T_DropSessionVarStmt: + lev = LOGSTMT_DDL; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 9f85eb86da1c..891674fbd454 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8816,6 +8816,14 @@ get_parameter(Param *param, deparse_context *context) } } + /* Note: can be be used by EXPLAIN */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "VARIABLE(%s)", + quote_identifier(param->paramvarname)); + return; + } + /* * Not PARAM_EXEC, or couldn't find referent: just print $N. * 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/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index b1ff6f6cd949..b5725d554c92 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1200,6 +1200,11 @@ Keywords_for_list_of_owner_roles, "PUBLIC" " FROM pg_catalog.pg_timezone_names() "\ " WHERE pg_catalog.quote_literal(pg_catalog.lower(name)) LIKE pg_catalog.lower('%s')" +#define Query_for_list_of_temporary_session_variables \ +"SELECT varname "\ +" FROM pg_catalog.pg_get_temporary_session_variables_names() AS varname "\ +" WHERE varname LIKE '%s'" + /* Privilege options shared between GRANT and REVOKE */ #define Privilege_options_of_grant_and_revoke \ "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", \ @@ -1265,8 +1270,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", @@ -1364,6 +1369,7 @@ static const pgsql_thing_t words_after_create[] = { {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, + {"VARIABLE", NULL, NULL, NULL, NULL, THING_NO_CREATE}, {NULL} /* end of list */ }; @@ -3737,7 +3743,7 @@ match_previous_words(int pattern_id, /* CREATE TABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete "CREATE TEMP/TEMPORARY" with the possible temp objects */ else if (TailMatches("CREATE", "TEMP|TEMPORARY")) - COMPLETE_WITH("SEQUENCE", "TABLE", "VIEW"); + COMPLETE_WITH("SEQUENCE", "TABLE", "VARIABLE", "VIEW"); /* Complete "CREATE UNLOGGED" with TABLE or SEQUENCE */ else if (TailMatches("CREATE", "UNLOGGED")) COMPLETE_WITH("TABLE", "SEQUENCE"); @@ -4090,6 +4096,13 @@ match_previous_words(int pattern_id, COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE */ + else if (Matches("CREATE", "TEMP|TEMPORARY", "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 */ else if (TailMatches("CREATE", "VIEW", MatchAny) || @@ -4366,6 +4379,10 @@ 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_QUERY(Query_for_list_of_temporary_session_variables); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4804,6 +4821,13 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* Complete LET with "=" */ + else if (Matches("LET")) + COMPLETE_WITH_QUERY(Query_for_list_of_temporary_session_variables); + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) @@ -5293,7 +5317,7 @@ match_previous_words(int pattern_id, COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, "VERBOSE", "ANALYZE", - "ONLY"); + "ONLY"); else if (Matches("VACUUM", MatchAnyN, "VERBOSE")) COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, "ANALYZE", @@ -5360,6 +5384,12 @@ match_previous_words(int pattern_id, */ } +/* + * VARIABLE fence + */ + else if (TailMatches("VARIABLE", "(")) + COMPLETE_WITH_QUERY(Query_for_list_of_temporary_session_variables); + /* WITH [RECURSIVE] */ /* diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fd9448ec7b98..ac08a4de8fe1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12612,4 +12612,9 @@ 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 => 'returns names of temporary session variables', + proname => 'pg_get_temporary_session_variables_names', prorows => '1000', proretset => 't', + provolatile => 'v', proparallel => 'r', prorettype => 'text', proargtypes => '', + prosrc => 'pg_get_temporary_session_variables_names' }, ] diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 000000000000..45ccbe2f0465 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,47 @@ +/*------------------------------------------------------------------------- + * + * 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 "nodes/params.h" +#include "parser/parse_node.h" +#include "nodes/parsenodes.h" +#include "tcop/cmdtag.h" + +extern void CreateVariable(ParseState *pstate, CreateSessionVarStmt *stmt); +extern void DropVariableByName(DropSessionVarStmt *stmt); + +extern Datum GetSessionVariableWithTypecheck(char *varname, Oid typid, int32 typmod, bool *isnull); +extern void SetSessionVariableWithTypecheck(char *varname, + Oid typid, int32 typmod, + Datum value, bool isnull); + +extern void get_session_variable_type_typmod_collid(char *varname, + Oid *typid, + int32 *typmod, + Oid *collid); + +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + +extern void ResetSessionVariables(void); + +extern void AtPreEOXact_SessionVariables(bool isCommit); +extern void AtEOSubXact_SessionVariables(bool isCommit, + SubTransactionId mySubid, + SubTransactionId parentSubid); + +#endif diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 000000000000..dd01c93c9e85 --- /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(char *varname); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 3968429f9919..11cb7f3580b7 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/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index bc7adba4a0fc..517bfd91f228 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 */ + char *resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -167,6 +170,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 +327,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; + char *varname; /* variable name */ + ParseLoc location; /* token location, or -1 if unknown */ +} VariableFence; + /* * A_Expr - infix, prefix, and postfix expressions */ @@ -2184,6 +2199,18 @@ typedef struct MergeStmt WithClause *withClause; /* WITH clause */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + char *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * @@ -3583,6 +3610,31 @@ typedef struct AlterStatsStmt bool missing_ok; /* skip error if statistics object is missing */ } AlterStatsStmt; + +/* ---------------------- + * Create Variable Statement + * ---------------------- + */ +typedef struct CreateSessionVarStmt +{ + NodeTag type; + char *name; /* the variable to create */ + TypeName *typeName; /* the type of variable */ + bool if_not_exists; /* just do nothing if variable already exists? */ +} CreateSessionVarStmt; + +/* ---------------------- + * DROP Variable Statement + * ---------------------- + */ +typedef struct DropSessionVarStmt +{ + NodeTag type; + char *name; /* the variable name to drop */ + bool missing_ok; /* skip error of variable is missing */ +} DropSessionVarStmt; + + /* ---------------------- * Create Function Statement * ---------------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index b5ff456ef7fa..f40845ecf19d 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -189,6 +189,12 @@ 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; + + /* name of session variable used like target of LET command */ + char *resultVariable; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -547,6 +553,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..61754ae4efce 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -158,6 +158,9 @@ typedef struct PlannedStmt */ List *extension_state; + /* 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/nodes/primnodes.h b/src/include/nodes/primnodes.h index 1b4436f2ff6d..760ca03f7e19 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 + * (paramvarname holds the variable's name). */ 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 */ + char *paramvarname pg_node_attr(query_jumble_ignore); /* token location, or -1 if unknown */ ParseLoc location; } Param; 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 */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 9fde58f541ca..92eaa4cfb245 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) @@ -491,6 +492,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/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c845425..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; @@ -228,6 +229,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index c4606d65043e..fd212257167e 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -123,6 +123,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 +176,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) @@ -183,6 +185,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 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/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 63598aba8a80..f9d206658b1c 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8267,7 +8267,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/test/regress/expected/session_variables_ddl.out b/src/test/regress/expected/session_variables_ddl.out new file mode 100644 index 000000000000..9031b5c384c9 --- /dev/null +++ b/src/test/regress/expected/session_variables_ddl.out @@ -0,0 +1,131 @@ +SET log_statement TO ddl; +-- should to fail +CREATE VARIABLE x AS int; +ERROR: only temporal session variables are supported +-- should be ok +CREATE TEMPORARY VARIABLE x AS int; +-- should fail +CREATE TEMPORARY VARIABLE x AS int; +ERROR: session variable "x" already exists +-- should fail +DROP VARIABLE y; +ERROR: session variable "y" doesn't exist +-- should be ok +DROP VARIABLE x; +CREATE TYPE test_type AS (x int, y int); +-- should fail +CREATE VARIABLE x AS test_type; +ERROR: only temporal session variables are supported +DROP TYPE test_type; +-- should fail +CREATE VARIABLE x AS int[]; +ERROR: only temporal session variables are supported +CREATE DOMAIN test_domain AS int; +-- should fail +CREATE TEMP VARIABLE x AS test_domain; +ERROR: test_domain is not a base type +DROP DOMAIN test_domain; +CREATE ROLE regress_session_variable_test_role_01; +CREATE ROLE regress_session_variable_test_role_02; +SET ROLE TO regress_session_variable_test_role_01; +CREATE TEMP VARIABLE x AS int; +SET ROLE TO default; +SET ROLE TO regress_session_variable_test_role_02; +-- should fail +DROP VARIABLE x; +ERROR: must be owner of session variable x +SET ROLE TO default; +SET ROLE TO regress_session_variable_test_role_01; +-- should be ok +DROP VARIABLE x; +SET ROLE TO DEFAULT; +DROP ROLE regress_session_variable_test_role_01; +DROP ROLE regress_session_variable_test_role_02; +CREATE TEMP VARIABLE x AS int; +-- should fail +CREATE TEMP VARIABLE x AS int; +ERROR: session variable "x" already exists +DISCARD TEMP; +-- should be ok +CREATE TEMP VARIABLE x AS int; +-- should be ok +CREATE TEMP VARIABLE IF NOT EXISTS x AS int; +NOTICE: session variable "x" already exists, skipping +DROP VARIABLE x; +DROP VARIABLE IF EXISTS x; +NOTICE: session variable "x" does not exists, skipping +CREATE TEMP VARIABLE x AS int; +CREATE TEMP VARIABLE y AS int; +SELECT * FROM pg_get_temporary_session_variables_names(); + pg_get_temporary_session_variables_names +------------------------------------------ + y + x +(2 rows) + +DROP VARIABLE x; +DROP VARIABLE y; +SELECT * FROM pg_get_temporary_session_variables_names(); + pg_get_temporary_session_variables_names +------------------------------------------ +(0 rows) + +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hi'; +BEGIN; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); + x +------- + Hello +(1 row) + +COMMIT; +SELECT VARIABLE(x); + x +------- + Hello +(1 row) + +LET x = 'Hi'; +BEGIN; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); + x +------- + Hello +(1 row) + +ROLLBACK; +SELECT VARIABLE(x); + x +---- + Hi +(1 row) + +BEGIN; +SAVEPOINT s1; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); + x +------- + Hello +(1 row) + +ROLLBACK TO s1; +SELECT VARIABLE(x); + x +---- + Hi +(1 row) + +COMMIT; +DROP VARIABLE x; 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..b87967bd7d51 --- /dev/null +++ b/src/test/regress/expected/session_variables_dml.out @@ -0,0 +1,258 @@ +CREATE TEMP VARIABLE temp_var01 AS int; +-- should not be accessible without variable's fence +-- should fail +SELECT temp_var01; +ERROR: column "temp_var01" does not exist +LINE 1: SELECT temp_var01; + ^ +-- should be ok +SELECT VARIABLE(temp_var01); + temp_var01 +------------ + +(1 row) + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(temp_var01); +END; +$$; +NOTICE: +-- variables cannot be used by persistent objects +-- that checks dependency +-- should fail +CREATE TEMP VIEW tempv AS SELECT VARIABLE(temp_var01); +ERROR: session variable "temp_var01" cannot be referenced in a catalog object +CREATE OR REPLACE FUNCTION testvar_sql() +RETURNS int AS $$ +SELECT VARIABLE(temp_var01); +$$ LANGUAGE sql; +SELECT testvar_sql(); + testvar_sql +------------- + +(1 row) + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE testvar_proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; +-- should not crash +CALL testvar_proc(VARIABLE(temp_var01)); +ERROR: session variable reference is not supported here +LINE 1: CALL testvar_proc(VARIABLE(temp_var01)); + ^ +PREPARE prepstmt(int) AS SELECT $1; +-- should not crash +EXECUTE prepstmt(VARIABLE(temp_var01)); +ERROR: session variable reference is not supported here +LINE 1: EXECUTE prepstmt(VARIABLE(temp_var01)); + ^ +DROP PROCEDURE testvar_proc; +DEALLOCATE prepstmt; +CREATE ROLE regress_session_variable_test_role_03; +CREATE OR REPLACE FUNCTION testvar_sd() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(temp_var01); +END; +$$ LANGUAGE plpgsql; +-- only owner can read data +SET ROLE TO regress_session_variable_test_role_03; +-- should fail +SELECT VARIABLE(temp_var01); +ERROR: permission denied for session variable temp_var01 +-- fx with security definer should be ok +SELECT testvar_sd(); +ERROR: permission denied for session variable temp_var01 +CONTEXT: PL/pgSQL expression "VARIABLE(temp_var01)" +PL/pgSQL function testvar_sd() line 3 at RAISE +SET ROLE TO default; +DROP VARIABLE temp_var01; +-- there is not plan cache invalidation +-- but still functions that uses dropped variables +-- should not to crash +SELECT testvar_sd(); +ERROR: session variable "temp_var01" doesn't exist +CONTEXT: PL/pgSQL expression "VARIABLE(temp_var01)" +PL/pgSQL function testvar_sd() line 3 at RAISE +SELECT testvar_sql(); +ERROR: session variable "temp_var01" doesn't exist +CONTEXT: SQL function "testvar_sql" during inlining +DROP FUNCTION testvar_sql(); +DROP FUNCTION testvar_sd(); +DROP ROLE regress_session_variable_test_role_03; +CREATE TABLE testvar_testtab(a int); +CREATE TEMP VARIABLE temp_var02 AS int; +INSERT INTO testvar_testtab SELECT * FROM generate_series(1,1000); +CREATE INDEX testvar_testtab_a ON testvar_testtab(a); +ANALYZE testvar_testtab; +-- force index +SET enable_seqscan TO OFF; +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02); + QUERY PLAN +------------------------------------------------------------ + Index Only Scan using testvar_testtab_a on testvar_testtab + Index Cond: (a = VARIABLE(temp_var02)) +(2 rows) + +DROP INDEX testvar_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 testvar_testtab WHERE a = 100; + QUERY PLAN +-------------------------------------------- + Gather + Workers Planned: 2 + -> Parallel Seq Scan on testvar_testtab + Filter: (a = 100) +(4 rows) + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02); + QUERY PLAN +-------------------------------------- + Seq Scan on testvar_testtab + Filter: (a = VARIABLE(temp_var02)) +(2 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +DROP TABLE testvar_testtab; +DROP VARIABLE temp_var02; +CREATE TEMP VARIABLE temp_var03 AS numeric; +-- LET stmt is not allowed inside CTE +WITH x AS (LET temp_var03 = 3.14) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET temp_var03 = 3.14) SELECT * FROM x; + ^ +-- LET stmt requires result with exactly one row +LET temp_var03 = generate_series(1,1); +SELECT VARIABLE(temp_var03); + temp_var03 +------------ + 1 +(1 row) + +-- should fail +LET temp_var03 = generate_series(1,2); +ERROR: expression returned more than one row +LET temp_var03 = generate_series(1,0); +ERROR: expression returned no rows +CREATE OR REPLACE FUNCTION testvar_sql01(numeric) +RETURNS void AS $$ +LET temp_var03 = $1; +$$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION testvar_sql02() +RETURNS numeric AS $$ +SELECT VARIABLE(temp_var03); +$$ LANGUAGE sql; +SELECT testvar_sql01(3.14); + testvar_sql01 +--------------- + +(1 row) + +SELECT testvar_sql02(), VARIABLE(temp_var03); + testvar_sql02 | temp_var03 +---------------+------------ + 3.14 | 3.14 +(1 row) + +CREATE OR REPLACE FUNCTION testvar_pl(varchar) +RETURNS varchar AS $$ +BEGIN + LET temp_var03 = $1::numeric; + RETURN VARIABLE(temp_var03); +END +$$ LANGUAGE plpgsql SECURITY DEFINER; +SELECT testvar_pl('3.14'); + testvar_pl +------------ + 3.14 +(1 row) + +DROP VARIABLE temp_var03; +SET plan_cache_mode to force_generic_plan; +-- should not crash +SELECT testvar_sql01(3.14); +ERROR: session variable "temp_var03" doesn't exist +CONTEXT: SQL function "testvar_sql01" during inlining +SELECT testvar_sql02(), VARIABLE(temp_var03); +ERROR: session variable "temp_var03" doesn't exist +SELECT testvar_pl('3.141592'); +ERROR: session variable "temp_var03" doesn't exist +CONTEXT: SQL statement "LET temp_var03 = $1::numeric" +PL/pgSQL function testvar_pl(character varying) line 3 at SQL statement +-- can work again if we create variable +CREATE TEMP VARIABLE temp_var03 AS numeric; +SELECT testvar_sql01(3.14); + testvar_sql01 +--------------- + +(1 row) + +SELECT testvar_sql02(), VARIABLE(temp_var03); + testvar_sql02 | temp_var03 +---------------+------------ + 3.14 | 3.14 +(1 row) + +SELECT testvar_pl('3.141592'); + testvar_pl +------------ + 3.141592 +(1 row) + +CREATE ROLE regress_session_variable_test_role_04; +SET ROLE regress_session_variable_test_role_04; +-- should fail +SELECT testvar_sql01(3.14); +ERROR: permission denied for session variable temp_var03 +CONTEXT: SQL function "testvar_sql01" statement 1 +-- should be ok (security definer) +SELECT testvar_pl('3.141592'); + testvar_pl +------------ + 3.141592 +(1 row) + +SET ROLE TO DEFAULT; +DROP FUNCTION testvar_sql01(numeric); +DROP FUNCTION testvar_sql02(); +DROP FUNCTION testvar_pl(varchar); +DROP ROLE regress_session_variable_test_role_04; +DROP VARIABLE temp_var03; +SET plan_cache_mode TO DEFAULT; +-- test extended query protocol +CREATE TEMP VARIABLE temp_var04 AS int; +LET temp_var04 = $1 \bind 10 \g +SELECT VARIABLE(temp_var04); + temp_var04 +------------ + 10 +(1 row) + +LET temp_var04 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(temp_var04); + temp_var04 +------------ + 100 +(1 row) + +\close_prepared letps +DROP VARIABLE temp_var04; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 905f9bca9598..d47b49b6088f 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 @@ -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_dml 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..5ec412ad7c90 --- /dev/null +++ b/src/test/regress/sql/session_variables_ddl.sql @@ -0,0 +1,113 @@ +SET log_statement TO ddl; + +-- should to fail +CREATE VARIABLE x AS int; + +-- should be ok +CREATE TEMPORARY VARIABLE x AS int; + +-- should fail +CREATE TEMPORARY VARIABLE x AS int; + +-- should fail +DROP VARIABLE y; + +-- should be ok +DROP VARIABLE x; + +CREATE TYPE test_type AS (x int, y int); + +-- should fail +CREATE VARIABLE x AS test_type; + +DROP TYPE test_type; + +-- should fail +CREATE VARIABLE x AS int[]; + +CREATE DOMAIN test_domain AS int; + +-- should fail +CREATE TEMP VARIABLE x AS test_domain; + +DROP DOMAIN test_domain; + +CREATE ROLE regress_session_variable_test_role_01; +CREATE ROLE regress_session_variable_test_role_02; + +SET ROLE TO regress_session_variable_test_role_01; + +CREATE TEMP VARIABLE x AS int; + +SET ROLE TO default; +SET ROLE TO regress_session_variable_test_role_02; + +-- should fail +DROP VARIABLE x; + +SET ROLE TO default; +SET ROLE TO regress_session_variable_test_role_01; + +-- should be ok +DROP VARIABLE x; + +SET ROLE TO DEFAULT; +DROP ROLE regress_session_variable_test_role_01; +DROP ROLE regress_session_variable_test_role_02; + +CREATE TEMP VARIABLE x AS int; + +-- should fail +CREATE TEMP VARIABLE x AS int; + +DISCARD TEMP; + +-- should be ok +CREATE TEMP VARIABLE x AS int; + +-- should be ok +CREATE TEMP VARIABLE IF NOT EXISTS x AS int; + +DROP VARIABLE x; +DROP VARIABLE IF EXISTS x; + +CREATE TEMP VARIABLE x AS int; +CREATE TEMP VARIABLE y AS int; +SELECT * FROM pg_get_temporary_session_variables_names(); + +DROP VARIABLE x; +DROP VARIABLE y; +SELECT * FROM pg_get_temporary_session_variables_names(); + +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hi'; +BEGIN; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); +COMMIT; +SELECT VARIABLE(x); + +LET x = 'Hi'; +BEGIN; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); +ROLLBACK; +SELECT VARIABLE(x); + +BEGIN; +SAVEPOINT s1; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +DROP VARIABLE x; +CREATE TEMP VARIABLE x AS varchar; +LET x = 'Hello'; +SELECT VARIABLE(x); +ROLLBACK TO s1; +SELECT VARIABLE(x); +COMMIT; + +DROP VARIABLE x; 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..b8408c97cad7 --- /dev/null +++ b/src/test/regress/sql/session_variables_dml.sql @@ -0,0 +1,207 @@ +CREATE TEMP VARIABLE temp_var01 AS int; + +-- should not be accessible without variable's fence +-- should fail +SELECT temp_var01; + +-- should be ok +SELECT VARIABLE(temp_var01); + +-- should not crash +DO $$ +BEGIN + RAISE NOTICE '%', VARIABLE(temp_var01); +END; +$$; + +-- variables cannot be used by persistent objects +-- that checks dependency +-- should fail +CREATE TEMP VIEW tempv AS SELECT VARIABLE(temp_var01); + +CREATE OR REPLACE FUNCTION testvar_sql() +RETURNS int AS $$ +SELECT VARIABLE(temp_var01); +$$ LANGUAGE sql; + +SELECT testvar_sql(); + +-- session variable cannot be used as parameter of CALL or EXECUTE +CREATE OR REPLACE PROCEDURE testvar_proc(int) +AS $$ +BEGIN + RAISE NOTICE '%', $1; +END; +$$ LANGUAGE plpgsql; + +-- should not crash +CALL testvar_proc(VARIABLE(temp_var01)); + +PREPARE prepstmt(int) AS SELECT $1; + +-- should not crash +EXECUTE prepstmt(VARIABLE(temp_var01)); + +DROP PROCEDURE testvar_proc; +DEALLOCATE prepstmt; + +CREATE ROLE regress_session_variable_test_role_03; + +CREATE OR REPLACE FUNCTION testvar_sd() +RETURNS void AS $$ +BEGIN + RAISE NOTICE '%', VARIABLE(temp_var01); +END; +$$ LANGUAGE plpgsql; + +-- only owner can read data +SET ROLE TO regress_session_variable_test_role_03; + +-- should fail +SELECT VARIABLE(temp_var01); + +-- fx with security definer should be ok +SELECT testvar_sd(); + +SET ROLE TO default; + +DROP VARIABLE temp_var01; + +-- there is not plan cache invalidation +-- but still functions that uses dropped variables +-- should not to crash + +SELECT testvar_sd(); +SELECT testvar_sql(); + +DROP FUNCTION testvar_sql(); +DROP FUNCTION testvar_sd(); + +DROP ROLE regress_session_variable_test_role_03; + +CREATE TABLE testvar_testtab(a int); +CREATE TEMP VARIABLE temp_var02 AS int; + +INSERT INTO testvar_testtab SELECT * FROM generate_series(1,1000); + +CREATE INDEX testvar_testtab_a ON testvar_testtab(a); + +ANALYZE testvar_testtab; + +-- force index +SET enable_seqscan TO OFF; + +-- index scan should be used +EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02); + +DROP INDEX testvar_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 testvar_testtab WHERE a = 100; + +-- parallel plan should not be used +EXPLAIN (COSTS OFF) SELECT * FROM testvar_testtab WHERE a = VARIABLE(temp_var02); + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +DROP TABLE testvar_testtab; +DROP VARIABLE temp_var02; + +CREATE TEMP VARIABLE temp_var03 AS numeric; + +-- LET stmt is not allowed inside CTE +WITH x AS (LET temp_var03 = 3.14) SELECT * FROM x; + +-- LET stmt requires result with exactly one row +LET temp_var03 = generate_series(1,1); +SELECT VARIABLE(temp_var03); + +-- should fail +LET temp_var03 = generate_series(1,2); +LET temp_var03 = generate_series(1,0); + +CREATE OR REPLACE FUNCTION testvar_sql01(numeric) +RETURNS void AS $$ +LET temp_var03 = $1; +$$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION testvar_sql02() +RETURNS numeric AS $$ +SELECT VARIABLE(temp_var03); +$$ LANGUAGE sql; + +SELECT testvar_sql01(3.14); +SELECT testvar_sql02(), VARIABLE(temp_var03); + +CREATE OR REPLACE FUNCTION testvar_pl(varchar) +RETURNS varchar AS $$ +BEGIN + LET temp_var03 = $1::numeric; + RETURN VARIABLE(temp_var03); +END +$$ LANGUAGE plpgsql SECURITY DEFINER; + +SELECT testvar_pl('3.14'); + +DROP VARIABLE temp_var03; + +SET plan_cache_mode to force_generic_plan; + +-- should not crash +SELECT testvar_sql01(3.14); +SELECT testvar_sql02(), VARIABLE(temp_var03); +SELECT testvar_pl('3.141592'); + +-- can work again if we create variable +CREATE TEMP VARIABLE temp_var03 AS numeric; +SELECT testvar_sql01(3.14); +SELECT testvar_sql02(), VARIABLE(temp_var03); +SELECT testvar_pl('3.141592'); + +CREATE ROLE regress_session_variable_test_role_04; + +SET ROLE regress_session_variable_test_role_04; + +-- should fail +SELECT testvar_sql01(3.14); + +-- should be ok (security definer) +SELECT testvar_pl('3.141592'); + +SET ROLE TO DEFAULT; + +DROP FUNCTION testvar_sql01(numeric); +DROP FUNCTION testvar_sql02(); +DROP FUNCTION testvar_pl(varchar); + +DROP ROLE regress_session_variable_test_role_04; + +DROP VARIABLE temp_var03; + +SET plan_cache_mode TO DEFAULT; + +-- test extended query protocol +CREATE TEMP VARIABLE temp_var04 AS int; + +LET temp_var04 = $1 \bind 10 \g +SELECT VARIABLE(temp_var04); + +LET temp_var04 = $1 \parse letps +\bind_named letps 100 \g +SELECT VARIABLE(temp_var04); + +\close_prepared letps + +DROP VARIABLE temp_var04; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3451538565e8..f399c3ab34a3 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -570,6 +570,7 @@ CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext CreateSeqStmt +CreateSessionVarStmt CreateStatsStmt CreateStmt CreateStmtContext @@ -673,6 +674,7 @@ DropBehavior DropOwnedStmt DropReplicationSlotCmd DropRoleStmt +DropSessionVarStmt DropStmt DropSubscriptionStmt DropTableSpaceStmt @@ -1558,6 +1560,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey @@ -2685,6 +2688,9 @@ SSL_CTX STARTUPINFO STRLEN SV +SVariableData +SVariable +SVariableState SYNCHRONIZATION_BARRIER SYSTEM_INFO SampleScan @@ -2743,6 +2749,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData @@ -3242,6 +3249,7 @@ ValidatorValidateCB ValuesScan ValuesScanState Var +VariableFence VarBit VarChar VarParamState