From ae2296ed3ae7a87ee4383b812da903acf4cc251a Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Thu, 6 Jul 2023 08:29:21 +0200 Subject: [PATCH 02/21] Storage for session variables and SQL interface Session variables are stored in session memory in a dedicated hash table. They are set by the LET command and read by the SELECT command. The access rights should be checked. The identifiers of session variables should always be shadowed by possible column identifiers: we don't want to break an application by creating some badly named session variable. The limits of this patch (solved by other patches): - session variables block parallel execution - session variables blocks simple expression evaluation (in plpgsql) - SQL functions with session variables are not inlined - CALL statement is not supported (usage of direct access to express executor) - EXECUTE statement is not supported (usage of direct access to express executor) - memory used by dropped session variables is not released Implementations of EXPLAIN LET and PREPARE LET statements are in separate patches (for better readability) --- doc/src/sgml/catalogs.sgml | 13 + doc/src/sgml/ddl.sgml | 61 + doc/src/sgml/func.sgml | 4 +- doc/src/sgml/glossary.sgml | 5 +- doc/src/sgml/parallel.sgml | 6 + doc/src/sgml/plpgsql.sgml | 14 + doc/src/sgml/ref/allfiles.sgml | 1 + doc/src/sgml/ref/alter_variable.sgml | 1 + doc/src/sgml/ref/create_variable.sgml | 5 +- doc/src/sgml/ref/drop_variable.sgml | 1 + doc/src/sgml/ref/let.sgml | 96 ++ doc/src/sgml/reference.sgml | 1 + src/backend/catalog/dependency.c | 5 + src/backend/catalog/namespace.c | 289 ++++ src/backend/catalog/pg_variable.c | 2 + src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/prepare.c | 10 + src/backend/commands/session_variable.c | 529 +++++++ src/backend/executor/Makefile | 1 + src/backend/executor/execExpr.c | 32 + src/backend/executor/execMain.c | 66 + src/backend/executor/meson.build | 1 + src/backend/executor/svariableReceiver.c | 172 +++ src/backend/nodes/nodeFuncs.c | 10 + src/backend/optimizer/plan/planner.c | 36 + src/backend/optimizer/plan/setrefs.c | 156 ++- src/backend/optimizer/prep/prepjointree.c | 3 + src/backend/optimizer/util/clauses.c | 33 +- src/backend/parser/analyze.c | 272 +++- src/backend/parser/gram.y | 39 +- src/backend/parser/parse_agg.c | 9 + src/backend/parser/parse_expr.c | 223 ++- src/backend/parser/parse_func.c | 2 + src/backend/parser/parse_merge.c | 1 + src/backend/tcop/dest.c | 7 + src/backend/tcop/utility.c | 16 + src/backend/utils/adt/ruleutils.c | 46 + src/backend/utils/cache/plancache.c | 41 +- src/backend/utils/fmgr/fmgr.c | 10 +- src/bin/psql/tab-complete.in.c | 12 +- src/include/catalog/namespace.h | 1 + src/include/catalog/pg_variable.h | 9 + src/include/commands/session_variable.h | 30 + src/include/executor/svariableReceiver.h | 22 + src/include/nodes/execnodes.h | 14 + src/include/nodes/parsenodes.h | 17 + src/include/nodes/pathnodes.h | 14 + src/include/nodes/plannodes.h | 9 + src/include/nodes/primnodes.h | 14 + src/include/optimizer/planmain.h | 2 + src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 3 + src/include/tcop/cmdtaglist.h | 1 + src/include/tcop/dest.h | 1 + src/pl/plpgsql/src/pl_exec.c | 3 +- .../regress/expected/session_variables.out | 1232 +++++++++++++++++ src/test/regress/sql/session_variables.sql | 883 ++++++++++++ src/tools/pgindent/typedefs.list | 5 + 59 files changed, 4467 insertions(+), 27 deletions(-) create mode 100644 doc/src/sgml/ref/let.sgml create mode 100644 src/backend/commands/session_variable.c create mode 100644 src/backend/executor/svariableReceiver.c create mode 100644 src/include/commands/session_variable.h create mode 100644 src/include/executor/svariableReceiver.h diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 8321b453e2c..125d755246a 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -9831,6 +9831,19 @@ SCRAM-SHA-256$<iteration count>:&l + + + varcreate_lsn pg_lsn + + + LSN of the transaction where the variable was created. + varcreate_lsn and + oid together form the all-time unique + identifier (oid alone is not enough, since + object identifiers can get reused). + + + varname name diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 6a493c2f561..fce5e72bbf6 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -5388,6 +5388,67 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; The session variable holds value in session memory. This value is private to each session and is released when the session ends. + + + The value of a session variable is set with the SQL statement + LET. The value of a session variable can be retrieved + with the SQL statement SELECT. + +CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; + + + or + + +CREATE VARIABLE public.current_user_id AS integer; +GRANT SELECT ON VARIABLE public.current_user_id TO PUBLIC; +LET current_user_id = (SELECT id FROM users WHERE usename = session_user); +SELECT current_user_id; + + + + + The value of a session variable is local to the current session. Retrieving + a variable's value returns a NULL, unless its value has + been set to something else in the current session using the + LET command. Session variables are not transactional: + any changes made to the value of a session variable in a transaction won't + be undone if the transaction is rolled back (just like variables in + procedural languages). Session variables themselves are persistent, but + their values are neither persistent nor shared (like the content of + temporary tables). + + + + Inside a query or an expression, a session variable can be + shadowed by a column with the same name. Similarly, the + name of a function or procedure argument or a PL/pgSQL variable (see + ) can shadow a session variable + in the routine's body. Such collisions of identifiers can be resolved + by using qualified identifiers: Session variables can be qualified with + the schema name, columns can use table aliases, routine variables can use + block labels, and routine arguments can use the routine name. + +CREATE VARIABLE name AS text; +LET name = 'John'; + +CREATE TABLE foo(name text); +INSERT INTO foo VALUES('Alice'); + +-- variable name is shadowed +SELECT name FROM foo; + + + which returns: + + + name +------- + Alice + + diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 010f7892fe0..37cf0c6435c 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -25847,8 +25847,8 @@ SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); 't' for TABLESPACE, 'F' for FOREIGN DATA WRAPPER, 'S' for FOREIGN SERVER, - or - 'T' for TYPE or DOMAIN. + 'T' for TYPE or DOMAIN or + 'V' for SESSION VARIABLE. diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index 83b13f98f28..2d13dcea4ef 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -1714,8 +1714,9 @@ A persistent database object that holds a value in session memory. This value is private to each session and is released when the session ends. - Read or write access to session variables is controlled by privileges, - similar to other database objects. + The default value of the session variable is null. Read or write access + to session variables is controlled by privileges, similar to other database + objects. For more information, see . diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f5..683dede6adc 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -515,6 +515,12 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; Plan nodes that reference a correlated SubPlan. + + + + Plan nodes that use a session variable. + + diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b8..1e4c43b8b61 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -6036,6 +6036,20 @@ $$ LANGUAGE plpgsql STRICT IMMUTABLE; + + + <command>Packages and package variables</command> + + + The PL/pgSQL language has no packages, and + therefore no package variables or package constants. + You can consider translating an Oracle package into a schema in + PostgreSQL. Package functions and procedures + would then become functions and procedures in that schema, and package + variables could be translated to session variables in that schema. + (see ). + + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index 2f67de3e21b..cc3bd5ab540 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -158,6 +158,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_variable.sgml b/doc/src/sgml/ref/alter_variable.sgml index 96d2586423e..221a699469b 100644 --- a/doc/src/sgml/ref/alter_variable.sgml +++ b/doc/src/sgml/ref/alter_variable.sgml @@ -173,6 +173,7 @@ ALTER VARIABLE boo SET SCHEMA private; + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml index 6e988f2e472..cd9ff63975a 100644 --- a/doc/src/sgml/ref/create_variable.sgml +++ b/doc/src/sgml/ref/create_variable.sgml @@ -120,9 +120,11 @@ CREATE VARIABLE [ IF NOT EXISTS ] nameExamples - Create an date session variable var1: + Create a session variable var1 of data type date: CREATE VARIABLE var1 AS date; +LET var1 = current_date; +SELECT var1; @@ -143,6 +145,7 @@ CREATE VARIABLE var1 AS date; + diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml index 5bdb3560f0b..67988b5fcd8 100644 --- a/doc/src/sgml/ref/drop_variable.sgml +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -111,6 +111,7 @@ DROP VARIABLE var1; + diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 00000000000..00f9bea91fe --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,96 @@ + + + + + LET + + + + session variable + changing + + + + LET + 7 + SQL - Language Statements + + + + LET + change a session variable's value + + + + +LET session_variable = sql_expression + + + + + Description + + + The LET command assigns a value to the specified session + variable. + + + + + + Parameters + + + + session_variable + + + The name of the session variable. + + + + + + sql_expression + + + An arbitrary SQL expression. The result must be of a data type that can + be cast to the type of the session variable in an assignment. + + + + + + + + + Examples + +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); + + + + + Compatibility + + + The LET is a PostgreSQL + extension. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 25578f3946c..13e4adc5df3 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -186,6 +186,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 9e0f648b74f..90c9e8bb254 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -1864,6 +1864,11 @@ find_expr_references_walker(Node *node, { Param *param = (Param *) node; + /* a variable parameter depends on the session variable */ + if (param->paramkind == PARAM_VARIABLE) + add_object_address(VariableRelationId, param->paramvarid, 0, + context->addrs); + /* A parameter must depend on the parameter's datatype */ add_object_address(TypeRelationId, param->paramtype, 0, context->addrs); diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index 4307cad15c7..beafee2a341 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3497,6 +3497,295 @@ NamesFromList(List *names) return result; } +/* ----- + * IdentifyVariable - try to find a variable from a list of identifiers + * + * Returns the OID of the variable found, or InvalidOid. + * + * "names" is a list of up to four identifiers; possible meanings are: + * - variable (searched on the search_path) + * - schema.variable + * - variable.attribute (searched on the search_path) + * - schema.variable.attribute + * - database.schema.variable + * - database.schema.variable.attribute + * + * If there is more than one way to identify a variable, "not_unique" will be + * set to true. + * + * Unless "noerror" is true, an error is raised if there are more than four + * identifiers in the list, or if the named database is not the current one. + * This is useful if we want to identify a shadowed variable. + * + * If an attribute is identified, it is stored in "attrname", otherwise the + * parameter is set to NULL. + * + * The identified session variable will be locked with an AccessShareLock. + * ----- + */ +Oid +IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror) +{ + Oid varid = InvalidOid; + Oid old_varid = InvalidOid; + uint64 inval_count; + bool retry = false; + + /* + * DDL operations can change the results of a name lookup. Since all such + * operations will generate invalidation messages, we keep track of + * whether any such messages show up while we're performing the operation, + * and retry until either (1) no more invalidation messages show up or (2) + * the answer doesn't change. + */ + for (;;) + { + Node *field1 = NULL; + Node *field2 = NULL; + Node *field3 = NULL; + Node *field4 = NULL; + char *a = NULL; + char *b = NULL; + char *c = NULL; + char *d = NULL; + Oid varoid_without_attr = InvalidOid; + Oid varoid_with_attr = InvalidOid; + + *not_unique = false; + *attrname = NULL; + varid = InvalidOid; + + inval_count = SharedInvalidMessageCounter; + + switch (list_length(names)) + { + case 1: + field1 = linitial(names); + + Assert(IsA(field1, String)); + + varid = LookupVariable(NULL, strVal(field1), true); + break; + + case 2: + field1 = linitial(names); + field2 = lsecond(names); + + Assert(IsA(field1, String)); + a = strVal(field1); + + if (IsA(field2, String)) + { + /* when both fields are of string type */ + b = strVal(field2); + + /* + * a.b can mean "schema"."variable" or + * "variable"."attribute". Check both variants, and + * returns InvalidOid with not_unique flag, when both + * interpretations are possible. + */ + varoid_without_attr = LookupVariable(a, b, true); + varoid_with_attr = LookupVariable(NULL, a, true); + } + else + { + /* the last field of list can be star too */ + Assert(IsA(field2, A_Star)); + + /* + * The syntax ident.* is used only by relation aliases, + * and then this identifier cannot be a reference to + * session variable. + */ + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = b; + varid = varoid_with_attr; + } + break; + + case 3: + { + bool field1_is_catalog = false; + + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + + a = strVal(field1); + b = strVal(field2); + + if (IsA(field3, String)) + { + c = strVal(field3); + + /* + * a.b.c can mean catalog.schema.variable or + * schema.variable.attribute. + * + * Check both variants, and set not_unique flag, when + * both interpretations are possible. + * + * When third node is star, only possible + * interpretation is schema.variable.*, but this + * pattern is not supported now. + */ + varoid_with_attr = LookupVariable(a, b, true); + + /* + * check pattern catalog.schema.variable only when + * there is possibility to success. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) == 0) + { + field1_is_catalog = true; + varoid_without_attr = LookupVariable(b, c, true); + } + } + else + { + Assert(IsA(field3, A_Star)); + return InvalidOid; + } + + if (OidIsValid(varoid_without_attr) && OidIsValid(varoid_with_attr)) + { + *not_unique = true; + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_without_attr)) + { + varid = varoid_without_attr; + } + else if (OidIsValid(varoid_with_attr)) + { + *attrname = c; + varid = varoid_with_attr; + } + + /* + * When we didn't find variable, we can (when it is + * allowed) raise cross-database reference error. + */ + if (!OidIsValid(varid) && !noerror && !field1_is_catalog) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + break; + + case 4: + { + field1 = linitial(names); + field2 = lsecond(names); + field3 = lthird(names); + field4 = lfourth(names); + + Assert(IsA(field1, String)); + Assert(IsA(field2, String)); + Assert(IsA(field3, String)); + + a = strVal(field1); + b = strVal(field2); + c = strVal(field3); + + /* + * In this case, "a" is used as catalog name - check it. + */ + if (strcmp(a, get_database_name(MyDatabaseId)) != 0) + { + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cross-database references are not implemented: %s", + NameListToString(names)))); + } + + if (IsA(field4, String)) + { + d = strVal(field4); + } + else + { + Assert(IsA(field4, A_Star)); + return InvalidOid; + } + + *attrname = d; + varid = LookupVariable(b, c, true); + } + break; + + default: + if (!noerror) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name (too many dotted names): %s", + NameListToString(names)))); + return InvalidOid; + } + + /* + * If, upon retry, we get back the same OID we did last time, then the + * invalidation messages we processed did not change the final answer. + * So we're done. + * + * If we got a different OID, we've locked the variable that used to + * have this name rather than the one that does now. So release the + * lock. + */ + if (retry) + { + if (old_varid == varid) + break; + + if (OidIsValid(old_varid)) + UnlockDatabaseObject(VariableRelationId, old_varid, 0, AccessShareLock); + } + + /* + * Lock the variable. This will also accept any pending invalidation + * messages. If we got back InvalidOid, indicating not found, then + * there's nothing to lock, but we accept invalidation messages + * anyway, to flush any negative catcache entries that may be + * lingering. + */ + if (!OidIsValid(varid)) + AcceptInvalidationMessages(); + else + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + /* + * If no invalidation message were processed, we're done! + */ + if (inval_count == SharedInvalidMessageCounter) + break; + + retry = true; + old_varid = varid; + varid = InvalidOid; + } + + return varid; +} + /* * DeconstructQualifiedName * Given a possibly-qualified name expressed as a list of String nodes, diff --git a/src/backend/catalog/pg_variable.c b/src/backend/catalog/pg_variable.c index 29d967a2d5b..bb445e9434b 100644 --- a/src/backend/catalog/pg_variable.c +++ b/src/backend/catalog/pg_variable.c @@ -26,6 +26,7 @@ #include "parser/parse_type.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/pg_lsn.h" #include "utils/syscache.h" static ObjectAddress create_variable(const char *varName, @@ -101,6 +102,7 @@ create_variable(const char *varName, varid = GetNewOidWithIndex(rel, VariableOidIndexId, Anum_pg_variable_oid); values[Anum_pg_variable_oid - 1] = ObjectIdGetDatum(varid); + values[Anum_pg_variable_varcreate_lsn - 1] = LSNGetDatum(GetXLogInsertRecPtr()); values[Anum_pg_variable_varname - 1] = NameGetDatum(&varname); values[Anum_pg_variable_varnamespace - 1] = ObjectIdGetDatum(varNamespace); values[Anum_pg_variable_vartype - 1] = ObjectIdGetDatum(varType); diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c60..aee40e7bd59 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -53,6 +53,7 @@ OBJS = \ schemacmds.o \ seclabel.o \ sequence.o \ + session_variable.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32..101c8d75dd1 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -41,6 +41,7 @@ backend_sources += files( 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'session_variable.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index bf7d2b2309f..56b3d8cb91d 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -342,6 +342,16 @@ EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, i++; } + /* + * The arguments of EXECUTE are evaluated by a direct expression + * executor call. This mode doesn't support session variables yet. + * It will be enabled later. + */ + if (pstate->p_hasSessionVariables) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable cannot be used as an argument"))); + /* Prepare the expressions for execution */ exprstates = ExecPrepareExprList(params, estate); diff --git a/src/backend/commands/session_variable.c b/src/backend/commands/session_variable.c new file mode 100644 index 00000000000..d2891a1dcb7 --- /dev/null +++ b/src/backend/commands/session_variable.c @@ -0,0 +1,529 @@ +/*------------------------------------------------------------------------- + * + * session_variable.c + * session variable creation/manipulation commands + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/session_variable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "rewrite/rewriteHandler.h" +#include "storage/lmgr.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* + * The values of session variables are stored in the backend's private memory + * in the dedicated memory context SVariableMemoryContext in binary format. + * They are stored in the "sessionvars" hash table, whose key is the OID of the + * variable. However, the OID is not good enough to identify a session + * variable: concurrent sessions could drop the session variable and create a + * new one, which could be assigned the same OID. To ensure that the values + * stored in memory and the catalog definition match, we also keep track of + * the "create_lsn". Before any access to the variable values, we need to + * check if the LSN stored in memory matches the LSN in the catalog. If there + * is a mismatch between the LSNs, or if the OID is not present in pg_variable + * at all, the value stored in memory is released. + */ +typedef struct SVariableData +{ + Oid varid; /* pg_variable OID of the variable (hash key) */ + XLogRecPtr create_lsn; + + bool isnull; + Datum value; + + Oid typid; + int16 typlen; + bool typbyval; + + bool is_domain; + + /* + * domain_check_extra holds cached domain metadata. This "extra" is + * usually stored in fn_mcxt. We do not have access to that memory context + * for session variables, but we can use TopTransactionContext instead. + * A fresh value is forced when we detect we are in a different transaction + * (the local transaction ID differs from domain_check_extra_lxid). + */ + void *domain_check_extra; + LocalTransactionId domain_check_extra_lxid; + + /* + * Stored value and type description can be outdated when we receive a + * sinval message. We then have to check if the stored data are still + * trustworthy. + */ + bool is_valid; + + uint32 hashvalue; /* used for pairing sinval message */ +} SVariableData; + +typedef SVariableData *SVariable; + +static HTAB *sessionvars = NULL; /* hash table for session variables */ + +static MemoryContext SVariableMemoryContext = NULL; + +/* + * Callback function for session variable invalidation. + */ +static void +pg_variable_cache_callback(Datum arg, int cacheid, uint32 hashvalue) +{ + HASH_SEQ_STATUS status; + SVariable svar; + + elog(DEBUG1, "pg_variable_cache_callback %u %u", cacheid, hashvalue); + + Assert(sessionvars); + + /* + * If the hashvalue is not specified, we have to recheck all currently + * used session variables. Since we can't tell the exact session variable + * from its hashvalue, we have to iterate over all items in the hash bucket. + */ + hash_seq_init(&status, sessionvars); + + while ((svar = (SVariable) hash_seq_search(&status)) != NULL) + { + if (hashvalue == 0 || svar->hashvalue == hashvalue) + { + svar->is_valid = false; + } + } +} + +/* + * Release stored value, free memory + */ +static void +free_session_variable_value(SVariable svar) +{ + /* clean the current value */ + if (!svar->isnull) + { + if (!svar->typbyval) + pfree(DatumGetPointer(svar->value)); + + svar->isnull = true; + } + + svar->value = (Datum) 0; +} + +/* + * Returns true when the entry in pg_variable is consistent with the given + * session variable. + */ +static bool +is_session_variable_valid(SVariable svar) +{ + HeapTuple tp; + bool result = false; + + Assert(OidIsValid(svar->varid)); + + tp = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(svar->varid)); + + if (HeapTupleIsValid(tp)) + { + /* + * The OID alone is not enough as an unique identifier, because OID + * values get recycled, and a new session variable could have got + * the same OID. We do a second check against the 64-bit LSN when + * the variable was created. + */ + if (svar->create_lsn == ((Form_pg_variable) GETSTRUCT(tp))->varcreate_lsn) + result = true; + + ReleaseSysCache(tp); + } + + return result; +} + +/* + * Initialize attributes cached in "svar" + */ +static void +setup_session_variable(SVariable svar, Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + + Assert(OidIsValid(varid)); + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for session variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + svar->varid = varid; + svar->create_lsn = varform->varcreate_lsn; + + svar->typid = varform->vartype; + + get_typlenbyval(svar->typid, &svar->typlen, &svar->typbyval); + + svar->is_domain = (get_typtype(varform->vartype) == TYPTYPE_DOMAIN); + svar->domain_check_extra = NULL; + svar->domain_check_extra_lxid = InvalidLocalTransactionId; + + svar->isnull = true; + svar->value = (Datum) 0; + + svar->is_valid = true; + + svar->hashvalue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + ReleaseSysCache(tup); +} + +/* + * Assign a new value to the session variable. It is copied to + * SVariableMemoryContext if necessary. + * + * If any error happens, the existing value won't be modified. + */ +static void +set_session_variable(SVariable svar, Datum value, bool isnull) +{ + Datum newval; + SVariableData locsvar, + *_svar; + + Assert(svar); + Assert(!isnull || value == (Datum) 0); + + /* + * Use typbyval, typbylen from session variable only when they are + * trustworthy (the invalidation message was not accepted for this + * variable). If the variable might be invalid, force setup. + * + * Do not overwrite the passed session variable until we can be certain + * that no error can be thrown. + */ + if (!svar->is_valid) + { + setup_session_variable(&locsvar, svar->varid); + _svar = &locsvar; + } + else + _svar = svar; + + if (!isnull) + { + MemoryContext oldcxt = MemoryContextSwitchTo(SVariableMemoryContext); + + newval = datumCopy(value, _svar->typbyval, _svar->typlen); + + MemoryContextSwitchTo(oldcxt); + } + else + newval = value; + + free_session_variable_value(svar); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has new value", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + svar->varid); + + /* no more error expected, so we can overwrite the old variable now */ + if (svar != _svar) + memcpy(svar, _svar, sizeof(SVariableData)); + + svar->value = newval; + svar->isnull = isnull; +} + +/* + * Create the hash table for storing session variables. + */ +static void +create_sessionvars_hashtables(void) +{ + HASHCTL vars_ctl; + + Assert(!sessionvars); + + if (!SVariableMemoryContext) + { + /* read sinval messages */ + CacheRegisterSyscacheCallback(VARIABLEOID, + pg_variable_cache_callback, + (Datum) 0); + + /* we need our own long-lived memory context */ + SVariableMemoryContext = + AllocSetContextCreate(TopMemoryContext, + "session variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&vars_ctl, 0, sizeof(vars_ctl)); + vars_ctl.keysize = sizeof(Oid); + vars_ctl.entrysize = sizeof(SVariableData); + vars_ctl.hcxt = SVariableMemoryContext; + + sessionvars = hash_create("Session variables", 64, &vars_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); +} + +/* + * Search a session variable in the hash table given its OID. If it + * doesn't exist, then insert it there. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the + * session variable until the end of the transaction. + */ +static SVariable +get_session_variable(Oid varid) +{ + SVariable svar; + bool found; + + /* protect the used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (found) + { + if (!svar->is_valid) + { + /* + * If there was an invalidation message, the variable might still be + * valid, but we have to check with the system catalog. + */ + if (is_session_variable_valid(svar)) + svar->is_valid = true; + else + /* if the value cannot be validated, we have to discard it */ + free_session_variable_value(svar); + } + } + else + svar->is_valid = false; + + /* + * Force setup for not yet initialized variables or variables that cannot + * be validated. + */ + if (!svar->is_valid) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by READ)", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + varid); + } + + /* ensure the returned data is still of the correct domain */ + if (svar->is_domain) + { + /* + * Store "extra" for domain_check() in TopTransactionContext. When we + * are in a new transaction, domain_check_extra cache is not valid any + * more. + */ + if (svar->domain_check_extra_lxid != MyProc->vxid.lxid) + svar->domain_check_extra = NULL; + + domain_check(svar->value, svar->isnull, + svar->typid, &svar->domain_check_extra, + TopTransactionContext); + + svar->domain_check_extra_lxid = MyProc->vxid.lxid; + } + + return svar; +} + +/* + * Store the given value in a session variable in the cache. + * + * The caller is responsible for doing permission checks. + * + * As a side effect, this function acquires a AccessShareLock on the session + * variable until the end of the transaction. + */ +void +SetSessionVariable(Oid varid, Datum value, bool isNull) +{ + SVariable svar; + bool found; + + /* protect used session variable against DROP */ + LockDatabaseObject(VariableRelationId, varid, 0, AccessShareLock); + + if (!sessionvars) + create_sessionvars_hashtables(); + + svar = (SVariable) hash_search(sessionvars, &varid, + HASH_ENTER, &found); + + if (!found) + { + setup_session_variable(svar, varid); + + elog(DEBUG1, "session variable \"%s.%s\" (oid:%u) has assigned entry in memory (emitted by WRITE)", + get_namespace_name(get_session_variable_namespace(svar->varid)), + get_session_variable_name(svar->varid), + varid); + } + + /* if this fails, it won't change the stored value */ + set_session_variable(svar, value, isNull); +} + +/* + * Returns a copy of the value stored in a variable. + */ +static inline Datum +copy_session_variable_value(SVariable svar, bool *isNull) +{ + Datum value; + + /* force copy of non NULL value */ + if (!svar->isnull) + { + value = datumCopy(svar->value, svar->typbyval, svar->typlen); + *isNull = false; + } + else + { + value = (Datum) 0; + *isNull = true; + } + + return value; +} + +/* + * Returns a copy of the value of the session variable (in the current memory + * context). The caller is responsible for permission checks. + */ +Datum +GetSessionVariable(Oid varid, bool *isNull) +{ + SVariable svar; + + svar = get_session_variable(varid); + + /* + * Although "svar" is freshly validated in this point, svar->is_valid can + * be false, if an invalidation message was processed during the domain check. + * But the variable and all its dependencies are locked now, so we don't need + * to repeat the validation. + */ + return copy_session_variable_value(svar, isNull); +} + +/* + * Assign the result of the evaluated expression to the session variable + */ +void +ExecuteLetStmt(ParseState *pstate, + LetStmt *stmt, + ParamListInfo params, + QueryEnvironment *queryEnv, + QueryCompletion *qc) +{ + Query *query = castNode(Query, stmt->query); + List *rewritten; + DestReceiver *dest; + AclResult aclresult; + PlannedStmt *plan; + QueryDesc *queryDesc; + Oid varid = query->resultVariable; + + Assert(OidIsValid(varid)); + + /* do we have permission to write to the session variable? */ + aclresult = object_aclcheck(VariableRelationId, varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_session_variable_name(varid)); + + /* create a dest receiver for LET */ + dest = CreateVariableDestReceiver(varid); + + /* run the query rewriter */ + query = copyObject(query); + + rewritten = QueryRewrite(query); + + Assert(list_length(rewritten) == 1); + + query = linitial_node(Query, rewritten); + Assert(query->commandType == CMD_SELECT); + + /* plan the query */ + plan = pg_plan_query(query, pstate->p_sourcetext, + CURSOR_OPT_PARALLEL_OK, params); + + /* + * Use a snapshot with an updated command ID to ensure this query sees the + * results of any previously executed queries. (This could only matter if + * the planner executed an allegedly-stable function that changed the + * database contents, but let's do it anyway to be parallel to the EXPLAIN + * code path.) + */ + PushCopiedSnapshot(GetActiveSnapshot()); + UpdateActiveSnapshotCommandId(); + + /* create a QueryDesc, redirecting output to our tuple receiver */ + queryDesc = CreateQueryDesc(plan, NULL, pstate->p_sourcetext, + GetActiveSnapshot(), InvalidSnapshot, + dest, params, queryEnv, 0); + + /* call ExecutorStart to prepare the plan for execution */ + ExecutorStart(queryDesc, 0); + + /* + * Run the plan to completion. The result should be only one row. To + * check if there are too many result rows, we try to fetch two. + */ + ExecutorRun(queryDesc, ForwardScanDirection, 2L); + + /* save the rowcount if we're given a QueryCompletion to fill */ + if (qc) + SetQueryCompletion(qc, CMDTAG_LET, queryDesc->estate->es_processed); + + /* and clean up */ + ExecutorFinish(queryDesc); + ExecutorEnd(queryDesc); + + FreeQueryDesc(queryDesc); + + PopActiveSnapshot(); +} diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile index 11118d0ce02..71248a34f26 100644 --- a/src/backend/executor/Makefile +++ b/src/backend/executor/Makefile @@ -76,6 +76,7 @@ OBJS = \ nodeWindowAgg.o \ nodeWorktablescan.o \ spi.o \ + svariableReceiver.o \ tqueue.o \ tstoreReceiver.o diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..e4e9de335b5 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -1041,6 +1041,38 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.d.param.paramtype = param->paramtype; ExprEvalPushStep(state, &scratch); break; + + case PARAM_VARIABLE: + { + int es_num_session_variables = 0; + SessionVariableValue *es_session_variables = NULL; + SessionVariableValue *var; + + if (state->parent && state->parent->state) + { + es_session_variables = state->parent->state->es_session_variables; + es_num_session_variables = state->parent->state->es_num_session_variables; + } + + Assert(es_session_variables); + + /* parameter sanity checks */ + if (param->paramid >= es_num_session_variables) + elog(ERROR, "paramid of PARAM_VARIABLE param is out of range"); + + var = &es_session_variables[param->paramid]; + + /* + * In this case, pass the value like a + * constant. + */ + scratch.opcode = EEOP_CONST; + scratch.d.constval.value = var->value; + scratch.d.constval.isnull = var->isnull; + ExprEvalPushStep(state, &scratch); + } + break; + case PARAM_EXTERN: /* diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 2da848970be..3161ac9f5f7 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -43,7 +43,9 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" +#include "catalog/pg_variable.h" #include "commands/matview.h" +#include "commands/session_variable.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" @@ -206,6 +208,70 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) Assert(queryDesc->sourceText != NULL); estate->es_sourceText = queryDesc->sourceText; + /* + * The executor doesn't work with session variables directly. Values of + * related session variables are copied to a dedicated array, and this array + * is passed to the executor. This array is stable "snapshot" of values of + * used session variables. There are three benefits of this strategy: + * + * - consistency with external parameters and plpgsql variables, + * + * - session variables can be parallel safe, + * + * - we don't need make fresh copy for any read of session variable + * (this is necessary because the internally the session variable can + * be changed inside query execution time, and then a reference to + * previously returned value can be corrupted). + */ + if (queryDesc->plannedstmt->sessionVariables) + { + int nSessionVariables; + int i = 0; + + /* + * In this case, the query uses session variables, but we have to + * prepare the array with passed values (of used session variables) + * first. + */ + Assert(!IsParallelWorker()); + nSessionVariables = list_length(queryDesc->plannedstmt->sessionVariables); + + /* create the array used for passing values of used session variables */ + estate->es_session_variables = (SessionVariableValue *) + palloc(nSessionVariables * sizeof(SessionVariableValue)); + + /* fill the array */ + foreach_oid(varid, queryDesc->plannedstmt->sessionVariables) + { + /* + * Permission check should be executed on all explicitly used + * variables in the query. For implicitly used variable + * (like base node of assignment indirect) we cannot do permission + * check, because we need read the value (and user can have + * only UPDATE variable). In this case the permission check + * is executed in write time. + */ + if (varid != queryDesc->plannedstmt->exclSelectPermCheckVarid) + { + AclResult aclresult; + + aclresult = object_aclcheck(VariableRelationId, varid, + GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, + get_session_variable_name(varid)); + } + + estate->es_session_variables[i].value = + GetSessionVariable(varid, + &estate->es_session_variables[i].isnull); + + i++; + } + + estate->es_num_session_variables = nSessionVariables; + } + /* * Fill in the query environment, if any, from queryDesc. */ diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f8771..491092fcc4c 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -64,6 +64,7 @@ backend_sources += files( 'nodeWindowAgg.c', 'nodeWorktablescan.c', 'spi.c', + 'svariableReceiver.c', 'tqueue.c', 'tstoreReceiver.c', ) diff --git a/src/backend/executor/svariableReceiver.c b/src/backend/executor/svariableReceiver.c new file mode 100644 index 00000000000..c6163fb36f6 --- /dev/null +++ b/src/backend/executor/svariableReceiver.c @@ -0,0 +1,172 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.c + * An implementation of DestReceiver that stores the result value in + * a session variable. + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/executor/svariableReceiver.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/detoast.h" +#include "catalog/pg_variable.h" +#include "commands/session_variable.h" +#include "executor/svariableReceiver.h" +#include "storage/lock.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* + * This DestReceiver is used by the LET command for storing the result to a + * session variable. The result has to have only one tuple with only one + * non-deleted attribute. The row counter (field "rows") is incremented + * after receiving a row, and an error is raised when there are no rows or + * there are more than one received rows. A received tuple cannot to have + * deleted attributes. The value is detoasted before storing it in the + * session variable. + */ +typedef struct +{ + DestReceiver pub; + Oid varid; + bool need_detoast; /* do we need to detoast the attribute? */ + int rows; /* row counter */ +} SVariableState; + +/* + * Prepare to receive tuples from executor. + */ +static void +svariableStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SVariableState *myState = (SVariableState *) self; + LOCKTAG locktag PG_USED_FOR_ASSERTS_ONLY; + Form_pg_attribute attr; + Oid typid PG_USED_FOR_ASSERTS_ONLY; + Oid collid PG_USED_FOR_ASSERTS_ONLY; + int32 typmod PG_USED_FOR_ASSERTS_ONLY; + + Assert(myState->pub.mydest == DestVariable); + Assert(OidIsValid(myState->varid)); + Assert(SearchSysCacheExists1(VARIABLEOID, myState->varid)); + Assert(typeinfo->natts == 1); + +#ifdef USE_ASSERT_CHECKING + + SET_LOCKTAG_OBJECT(locktag, + MyDatabaseId, + VariableRelationId, + myState->varid, + 0); + + Assert(LockHeldByMe(&locktag, AccessShareLock, false)); + +#endif + + attr = TupleDescAttr(typeinfo, 0); + + Assert(!attr->attisdropped); + +#ifdef USE_ASSERT_CHECKING + + get_session_variable_type_typmod_collid(myState->varid, + &typid, + &typmod, + &collid); + + Assert(attr->atttypid == typid); + Assert(attr->atttypmod < 0 || attr->atttypmod == typmod); + +#endif + + myState->need_detoast = attr->attlen == -1; + myState->rows = 0; +} + +/* + * Receive a tuple from the executor and store it in the session variable. + */ +static bool +svariableReceiveSlot(TupleTableSlot *slot, DestReceiver *self) +{ + SVariableState *myState = (SVariableState *) self; + Datum value; + bool isnull; + bool freeval = false; + + /* make sure the tuple is fully deconstructed */ + slot_getallattrs(slot); + + value = slot->tts_values[0]; + isnull = slot->tts_isnull[0]; + + if (myState->need_detoast && !isnull && VARATT_IS_EXTERNAL(DatumGetPointer(value))) + { + value = PointerGetDatum(detoast_external_attr((struct varlena *) + DatumGetPointer(value))); + freeval = true; + } + + myState->rows += 1; + + if (myState->rows > 1) + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ROWS), + errmsg("expression returned more than one row"))); + + SetSessionVariable(myState->varid, value, isnull); + + if (freeval) + pfree(DatumGetPointer(value)); + + return true; +} + +/* + * Clean up at end of the executor run + */ +static void +svariableShutdownReceiver(DestReceiver *self) +{ + if (((SVariableState *) self)->rows == 0) + ereport(ERROR, + (errcode(ERRCODE_NO_DATA_FOUND), + errmsg("expression returned no rows"))); +} + +/* + * Destroy the receiver when we are done with it + */ +static void +svariableDestroyReceiver(DestReceiver *self) +{ + pfree(self); +} + +/* + * Initially create a DestReceiver object. + */ +DestReceiver * +CreateVariableDestReceiver(Oid varid) +{ + SVariableState *self = (SVariableState *) palloc0(sizeof(SVariableState)); + + self->pub.receiveSlot = svariableReceiveSlot; + self->pub.rStartup = svariableStartupReceiver; + self->pub.rShutdown = svariableShutdownReceiver; + self->pub.rDestroy = svariableDestroyReceiver; + self->pub.mydest = DestVariable; + + self->varid = varid; + + return (DestReceiver *) self; +} diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..d8fc3d9fa69 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4375,6 +4375,16 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (WALK(stmt->target)) + return true; + if (WALK(stmt->query)) + return true; + } + break; case T_PLAssignStmt: { PLAssignStmt *stmt = (PLAssignStmt *) node; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 566ce5b3cb4..4a92888308a 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -338,6 +338,21 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->lastPlanNodeId = 0; glob->transientPlan = false; glob->dependsOnRole = false; + glob->sessionVariables = NIL; + + /* + * The (session) result variable should be stored to global, because + * it is not set in subquery. When this variable is used other than + * in base node of assignment indirection, we need to check the access + * rights (and then we need to detect this situation). The variable used + * like base node cannot be different than target (result) variable. + * Because we know the result variable before planner invocation, we + * can simply search of usage just this variable, and we don't need to + * to wait until the end of planning when we know basenodeSessionVarid. + */ + glob->resultVariable = parse->resultVariable; + glob->basenodeSessionVarid = InvalidOid; + glob->basenodeSessionVarSelectCheck = false; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -576,6 +591,20 @@ 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; + + /* + * The session variable used (and only used) like base node + * for assignemnt indirection should be excluded from permission + * check. + */ + if (OidIsValid(glob->basenodeSessionVarid) && + (!glob->basenodeSessionVarSelectCheck)) + result->exclSelectPermCheckVarid = glob->basenodeSessionVarid; + else + result->exclSelectPermCheckVarid = InvalidOid; + result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -754,6 +783,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ pull_up_subqueries(root); + /* + * Check if some subquery uses a session variable. The flag + * hasSessionVariables should be true if the query or some subquery uses a + * session variable. + */ + pull_up_has_session_variables(root); + /* * If this is a simple UNION ALL query, flatten it into an appendrel. We * do this now because it requires applying pull_up_subqueries to the leaf diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 150e9f060ee..d071bd0c559 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -210,6 +210,9 @@ static List *set_returning_clause_references(PlannerInfo *root, static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static bool pull_up_has_session_variables_walker(Node *node, + PlannerInfo *root); +static void record_plan_variable_dependency(PlannerInfo *root, Oid varid); /***************************************************************************** @@ -1318,6 +1321,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 @@ -2018,8 +2065,9 @@ copyVar(Var *var) * This is code that is common to all variants of expression-fixing. * We must look up operator opcode info for OpExpr and related nodes, * add OIDs from regclass Const nodes into root->glob->relationOids, and - * add PlanInvalItems for user-defined functions into root->glob->invalItems. - * We also fill in column index lists for GROUPING() expressions. + * add PlanInvalItems for user-defined functions and session variables into + * root->glob->invalItems. We also fill in column index lists for GROUPING() + * expressions. * * We assume it's okay to update opcode info in-place. So this could possibly * scribble on the planner's input data structures, but it's OK. @@ -2109,6 +2157,13 @@ fix_expr_common(PlannerInfo *root, Node *node) g->cols = cols; } } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + record_plan_variable_dependency(root, p->paramvarid); + } } /* @@ -2118,6 +2173,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) @@ -2136,6 +2195,62 @@ fix_param_node(PlannerInfo *root, Param *p) elog(ERROR, "unexpected PARAM_MULTIEXPR ID: %d", p->paramid); return copyObject(list_nth(params, colno - 1)); } + + if (p->paramkind == PARAM_VARIABLE) + { + int n = 0; + bool found = false; + + /* we will modify object */ + p = (Param *) copyObject(p); + + /* + * Now, we can actualize list of session variables, and we can + * complete paramid parameter. + */ + foreach_oid(varid, root->glob->sessionVariables) + { + if (varid == p->paramvarid) + { + p->paramid = n; + found = true; + break; + } + n += 1; + } + + if (!found) + { + root->glob->sessionVariables = lappend_oid(root->glob->sessionVariables, + p->paramvarid); + p->paramid = n; + } + + /* + * We do SELECT permission check of all variables used by + * the query excluding the variable that is used only as base node + * of assignment indirection. The variable id assigned to this param + * should be same like resultVariable id, and this param should be + * used only once in query. When the variable is referenced by any + * other param, we should to do SELECT permission check for this variable + * too. + */ + if (p->parambasenode) + { + Assert(!OidIsValid(root->glob->basenodeSessionVarid)); + Assert(root->glob->resultVariable == p->paramvarid); + + root->glob->basenodeSessionVarid = p->paramvarid; + } + else + { + if (p->paramvarid == root->glob->resultVariable) + root->glob->basenodeSessionVarSelectCheck = true; + } + + return (Node *) p; + } + return (Node *) copyObject(p); } @@ -2197,7 +2312,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, * replacing Aggref nodes that should be replaced by initplan output Params, * choosing the best implementation for AlternativeSubPlans, * looking up operator opcode info for OpExpr and related nodes, - * and adding OIDs from regclass Const nodes into root->glob->relationOids. + * adding OIDs from regclass Const nodes into root->glob->relationOids, + * assigning paramvarid to PARAM_VARIABLE params, and collecting the + * OIDs of session variables in the root->glob->sessionVariables list + * (paramvarid is the position of the session variable in this list). * * 'node': the expression to be modified * 'rtoffset': how much to increment varnos by @@ -2219,7 +2337,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); } @@ -3612,6 +3731,25 @@ record_plan_type_dependency(PlannerInfo *root, Oid typid) } } +/* + * Record dependency on a session variable. The variable can be used as a + * session variable in an expression list, or as the target of a LET statement. + */ +static void +record_plan_variable_dependency(PlannerInfo *root, Oid varid) +{ + PlanInvalItem *inval_item = makeNode(PlanInvalItem); + + /* paramid is still session variable id */ + inval_item->cacheId = VARIABLEOID; + inval_item->hashValue = GetSysCacheHashValue1(VARIABLEOID, + ObjectIdGetDatum(varid)); + + /* append this variable to global, register dependency */ + root->glob->invalItems = lappend(root->glob->invalItems, + inval_item); +} + /* * extract_query_dependencies * Given a rewritten, but not yet planned, query or queries @@ -3697,9 +3835,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) } /* - * Ignore other utility statements, except those (such as EXPLAIN) - * that contain a parsed-but-not-planned query. For those, we - * just need to transfer our attention to the contained query. + * Ignore other utility statements, except those (such as EXPLAIN + * or LET) that contain a parsed-but-not-planned query. For those, + * we just need to transfer our attention to the contained query. */ query = UtilityContainsQuery(query->utilityStmt); if (query == NULL) @@ -3722,6 +3860,10 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) lappend_oid(context->glob->relationOids, rte->relid); } + /* record dependency on the target variable of a LET command */ + if (OidIsValid(query->resultVariable)) + record_plan_variable_dependency(context, query->resultVariable); + /* And recurse into the query's subexpressions */ return query_tree_walker(query, extract_query_dependencies_walker, context, 0); diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index d131a5bbc59..6fd04dadfcf 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -1573,6 +1573,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 4622eaf709f..f9f00ada583 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -24,6 +24,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/session_variable.h" #include "executor/executor.h" #include "executor/functions.h" #include "funcapi.h" @@ -934,6 +935,13 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) if (param->paramkind == PARAM_EXTERN) return false; + /* we don't support passing session variables to workers */ + if (param->paramkind == PARAM_VARIABLE) + { + if (max_parallel_hazard_test(PROPARALLEL_RESTRICTED, context)) + return true; + } + if (param->paramkind != PARAM_EXEC || !list_member_int(context->safe_param_ids, param->paramid)) { @@ -2389,6 +2397,7 @@ convert_saop_to_hashed_saop_walker(Node *node, void *context) * value of the Param. * 2. Fold stable, as well as immutable, functions to constants. * 3. Reduce PlaceHolderVar nodes to their contained expressions. + * 4. Current value of session variable can be used for estimation too. *-------------------- */ Node * @@ -2515,6 +2524,27 @@ eval_const_expressions_mutator(Node *node, } } } + else if (param->paramkind == PARAM_VARIABLE && + context->estimate) + { + int16 typLen; + bool typByVal; + Datum pval; + bool isnull; + + get_typlenbyval(param->paramtype, + &typLen, &typByVal); + + pval = GetSessionVariable(param->paramvarid, &isnull); + + return (Node *) makeConst(param->paramtype, + param->paramtypmod, + param->paramcollid, + (int) typLen, + pval, + isnull, + typByVal); + } /* * Not replaceable, so just copy the Param (no need to @@ -4720,7 +4750,8 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid, querytree->limitOffset || querytree->limitCount || querytree->setOperations || - list_length(querytree->targetList) != 1) + (list_length(querytree->targetList) != 1) || + querytree->hasSessionVariables) goto fail; /* If the function result is composite, resolve it */ diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 76f58b3aca3..725f678e2fa 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,9 +25,12 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" +#include "commands/session_variable.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -51,6 +54,7 @@ #include "utils/backend_status.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -83,6 +87,8 @@ static Query *transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt); static Query *transformCallStmt(ParseState *pstate, CallStmt *stmt); +static Query *transformLetStmt(ParseState *pstate, + LetStmt *stmt); static void transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc, bool pushedDown); #ifdef DEBUG_NODE_TESTS_ENABLED @@ -414,6 +420,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_UpdateStmt: case T_DeleteStmt: case T_MergeStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -493,6 +500,11 @@ transformStmt(ParseState *pstate, Node *parseTree) (CallStmt *) parseTree); break; + case T_LetStmt: + result = transformLetStmt(pstate, + (LetStmt *) parseTree); + break; + default: /* @@ -545,6 +557,7 @@ stmt_requires_parse_analysis(RawStmt *parseTree) case T_SelectStmt: case T_ReturnStmt: case T_PLAssignStmt: + case T_LetStmt: result = true; break; @@ -653,6 +666,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) qry->hasWindowFuncs = pstate->p_hasWindowFuncs; qry->hasTargetSRFs = pstate->p_hasTargetSRFs; qry->hasAggs = pstate->p_hasAggs; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); @@ -1078,6 +1092,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); @@ -1543,6 +1558,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) { @@ -1769,12 +1785,241 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *query; + Query *result; + List *exprList = NIL; + List *exprListCoer = NIL; + ListCell *lc; + ListCell *indirection_head = NULL; + Query *selectQuery; + Oid varid; + char *attrname = NULL; + bool not_unique; + bool is_rowtype; + Oid typid; + int32 typmod; + Oid collid; + List *names = NULL; + int indirection_start; + int i = 0; + + /* there can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + names = NamesFromList(stmt->target); + + /* locks the variable with an AccessShareLock */ + varid = IdentifyVariable(names, &attrname, ¬_unique, false); + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("target \"%s\" of LET command is ambiguous", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + if (!OidIsValid(varid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("session variable \"%s\" doesn't exist", + NameListToString(names)), + parser_errposition(pstate, stmt->location))); + + /* + * Calculate start of possible position of an indirection in list, and + * when it is inside the list, store pointer on first node of indirection. + */ + indirection_start = list_length(names) - (attrname ? 1 : 0); + if (list_length(stmt->target) > indirection_start) + indirection_head = list_nth_cell(stmt->target, indirection_start); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, &collid); + + is_rowtype = type_is_rowtype(typid); + + if (attrname && !is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot assign to field \"%s\" of session variable \"%s.%s\" because its type %s is not a composite type", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, stmt->location))); + + pstate->p_expr_kind = EXPR_KIND_LET_TARGET; + + /* we need to postpone conversion of "unknown" to text */ + pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(pstate, stmt->query); + + /* the grammar should have produced a SELECT */ + Assert(IsA(selectQuery, Query) && selectQuery->commandType == CMD_SELECT); + + /* + * Generate an expression list for the LET that selects all the non-resjunk + * columns from the subquery. + */ + exprList = NIL; + foreach_node(TargetEntry, tle, selectQuery->targetList) + { + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* don't allow multicolumn result */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg_plural("assignment expression returned %d column", + "assignment expression returned %d columns", + list_length(exprList), + list_length(exprList)), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + + foreach(lc, exprList) + { + Expr *expr = (Expr *) lfirst(lc); + Expr *coerced_expr; + Oid exprtypid; + + /* now we can read the type of the expression */ + exprtypid = exprType((Node *) expr); + + if (indirection_head) + { + bool targetIsArray; + char *targetName; + Param *param; + + targetName = get_session_variable_name(varid); + targetIsArray = OidIsValid(get_element_type(typid)); + + pstate->p_hasSessionVariables = true; + + param = makeNode(Param); + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + + /* + * The parameter used as basenode has to have special + * mark, because requires special access when we do + * SELECT access check. + */ + param->parambasenode = true; + + coerced_expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + targetName, + targetIsArray, + typid, + typmod, + InvalidOid, + stmt->target, + indirection_head, + (Node *) expr, + COERCION_ASSIGNMENT, + stmt->location); + } + else + coerced_expr = (Expr *) + coerce_to_target_type(pstate, + (Node *) expr, + exprtypid, + typid, typmod, + COERCION_ASSIGNMENT, + COERCE_IMPLICIT_CAST, + stmt->location); + + if (coerced_expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s.%s\" is of type %s, but expression is of type %s", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) expr)))); + + exprListCoer = lappend(exprListCoer, coerced_expr); + } + + /* generate query's target list using the computed list of expressions */ + query = makeNode(Query); + query->commandType = CMD_SELECT; + + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *) expr), + false); + query->targetList = lappend(query->targetList, tle); + } + + /* done building the range table and jointree */ + query->rtable = pstate->p_rtable; + query->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + query->hasTargetSRFs = pstate->p_hasTargetSRFs; + query->hasSubLinks = pstate->p_hasSubLinks; + query->hasSessionVariables = pstate->p_hasSessionVariables; + + /* this is top-level query */ + query->canSetTag = true; + + /* + * Save target session variable ID. It is used later for + * acquiring an AccessShareLock on target variable, setting + * plan dependency and finally for creating VariableDestReceiver. + */ + query->resultVariable = varid; + + assign_query_collations(pstate, query); + + /* + * The query is executed as utility command by nested executor call. + * Assigned queryId is required in this case. + */ + if (IsQueryIdEnabled()) + JumbleQuery(query); + + stmt->query = (Node *) query; + + /* represent the command as a utility Query */ + result = makeNode(Query); + result->commandType = CMD_UTILITY; + result->utilityStmt = (Node *) stmt; + + return result; +} + /* * transformSetOperationStmt - * transforms a set-operations tree @@ -2020,6 +2265,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) { @@ -2495,6 +2741,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); @@ -2562,6 +2809,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); @@ -2852,9 +3100,15 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Transform the target reference. Typically we will get back a Param * node, but there's no reason to be too picky about its type. + * + * Session variables should not be used as target of a PL/pgSQL assign + * statement. So we should use a dedicated expression kind and disallow + * session variables there. The dedicated context allows to eliminate + * undesirable warnings about the possibility of a target PL/pgSQL variable + * shadowing a session variable. */ target = transformExpr(pstate, (Node *) cref, - EXPR_KIND_UPDATE_TARGET); + EXPR_KIND_ASSIGN_TARGET); targettype = exprType(target); targettypmod = exprTypmod(target); targetcollation = exprCollation(target); @@ -2896,6 +3150,10 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) */ type_id = exprType((Node *) tle->expr); + /* + * For indirection processing and additional casts we can use expr_kind + * EXPR_KIND_UPDATE_TARGET. + */ pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; if (indirection) @@ -3038,6 +3296,8 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) (LockingClause *) lfirst(l), false); } + qry->hasSessionVariables = pstate->p_hasSessionVariables; + assign_query_collations(pstate, qry); /* this must be done after collations, for reliable comparison of exprs */ @@ -3311,6 +3571,16 @@ transformCallStmt(ParseState *pstate, CallStmt *stmt) true, stmt->funccall->location); + /* + * The arguments of CALL statement are evaluated by a direct expression + * executor call. This path is unsupported yet, so block it. It will be + * enabled later. + */ + if (pstate->p_hasSessionVariables) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("session variable cannot be used as an argument"))); + assign_expr_collations(pstate, node); fexpr = castNode(FuncExpr, node); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index cf7eee458a3..8de7041e27b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -297,7 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -742,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEEP KEY KEYS LABEL LANGUAGE LARGE_P LAST_P LATERAL_P - LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL + LEADING LEAKPROOF LEAST LEFT LET LEVEL LIKE LIMIT LISTEN LOAD LOCAL LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD @@ -1089,6 +1089,7 @@ stmt: | ImportForeignSchemaStmt | IndexStmt | InsertStmt + | LetStmt | ListenStmt | RefreshMatViewStmt | LoadStmt @@ -12863,6 +12864,38 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENT + * + *****************************************************************************/ +LetStmt: LET ColId opt_indirection '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select; + ResTarget *res; + + n->target = lcons(makeString($2), + check_indirection($3, yyscanner)); + + select = makeNode(SelectStmt); + res = makeNode(ResTarget); + + /* create target list for implicit query */ + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $5; + res->location = @5; + + select->targetList = list_make1(res); + n->query = (Node *) select; + + n->location = @2; + $$ = (Node *) n; + } + ; + /***************************************************************************** * * QUERY: @@ -17934,6 +17967,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -18547,6 +18581,7 @@ bare_label_keyword: | LEAKPROOF | LEAST | LEFT + | LET | LEVEL | LIKE | LISTEN diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 0ac8966e30f..de0a4a5653a 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -580,6 +580,11 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -970,6 +975,10 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 9caf1e481a2..55e4906ad76 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -17,6 +17,7 @@ #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/dbcommands.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -33,11 +34,13 @@ #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "parser/parse_type.h" +#include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" /* GUC parameters */ @@ -106,6 +109,9 @@ static Expr *make_distinct_op(ParseState *pstate, List *opname, Node *ltree, Node *rtree, int location); static Node *make_nulltest_from_distinct(ParseState *pstate, A_Expr *distincta, Node *arg); +static Node *makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location); /* @@ -499,6 +505,89 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) return result; } +/* + * Returns true if the given expression kind is valid for session variables. + * Session variables can be used everywhere where external parameters can be + * used. Session variables are not allowed in DDL commands or in constraints. + * + * An identifier can be parsed as a session variable only for expression kinds + * where session variables are allowed. This is the primary usage of this + * function. + * + * The second usage of this function is to decide whether a "column does not + * exist" or a "column or variable does not exist" error message should be + * printed. When we are in an expression where session variables cannot be + * used, we raise the first form of error message. + */ +static bool +expr_kind_allows_session_variables(ParseExprKind p_expr_kind) +{ + bool result = false; + + switch (p_expr_kind) + { + case EXPR_KIND_NONE: + Assert(false); /* can't happen */ + return false; + + /* session variables allowed */ + case EXPR_KIND_OTHER: + case EXPR_KIND_JOIN_ON: + case EXPR_KIND_FROM_SUBSELECT: + case EXPR_KIND_FROM_FUNCTION: + case EXPR_KIND_WHERE: + case EXPR_KIND_HAVING: + case EXPR_KIND_FILTER: + case EXPR_KIND_WINDOW_PARTITION: + case EXPR_KIND_WINDOW_ORDER: + case EXPR_KIND_WINDOW_FRAME_RANGE: + case EXPR_KIND_WINDOW_FRAME_ROWS: + case EXPR_KIND_WINDOW_FRAME_GROUPS: + case EXPR_KIND_SELECT_TARGET: + case EXPR_KIND_INSERT_TARGET: + case EXPR_KIND_UPDATE_SOURCE: + case EXPR_KIND_UPDATE_TARGET: + case EXPR_KIND_MERGE_WHEN: + case EXPR_KIND_MERGE_RETURNING: + case EXPR_KIND_GROUP_BY: + case EXPR_KIND_ORDER_BY: + case EXPR_KIND_DISTINCT_ON: + case EXPR_KIND_LIMIT: + case EXPR_KIND_OFFSET: + case EXPR_KIND_RETURNING: + case EXPR_KIND_VALUES: + case EXPR_KIND_VALUES_SINGLE: + case EXPR_KIND_ALTER_COL_TRANSFORM: + case EXPR_KIND_EXECUTE_PARAMETER: + case EXPR_KIND_POLICY: + case EXPR_KIND_CALL_ARGUMENT: + case EXPR_KIND_COPY_WHERE: + case EXPR_KIND_LET_TARGET: + result = true; + break; + + /* session variables not allowed */ + case EXPR_KIND_CHECK_CONSTRAINT: + case EXPR_KIND_DOMAIN_CHECK: + case EXPR_KIND_COLUMN_DEFAULT: + case EXPR_KIND_FUNCTION_DEFAULT: + case EXPR_KIND_INDEX_EXPRESSION: + case EXPR_KIND_INDEX_PREDICATE: + case EXPR_KIND_STATS_EXPRESSION: + case EXPR_KIND_TRIGGER_WHEN: + case EXPR_KIND_PARTITION_BOUND: + case EXPR_KIND_PARTITION_EXPRESSION: + case EXPR_KIND_GENERATED_COLUMN: + case EXPR_KIND_JOIN_USING: + case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + result = false; + break; + } + + return result; +} + /* * Transform a ColumnRef. * @@ -575,6 +664,8 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: /* okay */ break; @@ -847,8 +938,61 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) } /* - * Throw error if no translation found. + * There are contexts where session variables are not allowed. We don't + * need to identify session variables in such a context, but identifying + * them allows us to raise meaningful error messages like "you cannot use + * session variables here". */ + if (expr_kind_allows_session_variables(pstate->p_expr_kind)) + { + Oid varid = InvalidOid; + char *attrname = NULL; + bool not_unique; + + /* ----- + * Session variables are shadowed by columns, routine variables or + * routine arguments. We certainly don't want to use a session variable + * when it is exactly shadowed, but a RTE like this is conceivable: + * + * CREATE TYPE t AS (c int); + * CREATE VARIABLE foo AS t; + * CREATE TABLE foo(a int, b int); + * + * SELECT foo.a, foo.b, foo.c FROM foo; + * + * However, that is very confusing, so we disallow it. We don't try to + * identify a variable if we know that it would be shadowed. + * ----- + */ + if (!node && !(relname && crerr == CRERR_NO_COLUMN)) + { + /* takes an AccessShareLock on the session variable */ + varid = IdentifyVariable(cref->fields, &attrname, ¬_unique, false); + + if (OidIsValid(varid)) + { + Oid typid; + int32 typmod; + Oid collid; + + if (not_unique) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_PARAMETER), + errmsg("session variable reference \"%s\" is ambiguous", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + + get_session_variable_type_typmod_collid(varid, &typid, &typmod, + &collid); + + node = makeParamSessionVariable(pstate, + varid, typid, typmod, collid, + attrname, cref->location); + } + } + } + + /* throw an error if no translation was found */ if (node == NULL) { switch (crerr) @@ -880,6 +1024,75 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } +/* + * Generate param variable for reference to session variable + */ +static Node * +makeParamSessionVariable(ParseState *pstate, + Oid varid, Oid typid, int32 typmod, Oid collid, + char *attrname, int location) +{ + Param *param; + + param = makeNode(Param); + + param->paramkind = PARAM_VARIABLE; + param->paramvarid = varid; + param->paramtype = typid; + param->paramtypmod = typmod; + param->paramcollid = collid; + + pstate->p_hasSessionVariables = true; + + if (attrname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc_noerror(typid, typmod, true); + if (!tupdesc) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is of type \"%s\", which is not a composite type", + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid), + format_type_be(typid)), + parser_errposition(pstate, location))); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(attrname, NameStr(att->attname)) == 0 && + !att->attisdropped) + { + /* success, so generate a FieldSelect expression */ + FieldSelect *fselect = makeNode(FieldSelect); + + fselect->arg = (Expr *) param; + fselect->fieldnum = i + 1; + fselect->resulttype = att->atttypid; + fselect->resulttypmod = att->atttypmod; + /* save attribute's collation for parse_collate.c */ + fselect->resultcollid = att->attcollation; + + ReleaseTupleDesc(tupdesc); + return (Node *) fselect; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("could not identify column \"%s\" in variable \"%s.%s\"", + attrname, + get_namespace_name(get_session_variable_namespace(varid)), + get_session_variable_name(varid)), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} + static Node * transformParamRef(ParseState *pstate, ParamRef *pref) { @@ -1815,6 +2028,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_VALUES: case EXPR_KIND_VALUES_SINGLE: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_LET_TARGET: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: @@ -1858,6 +2072,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_ASSIGN_TARGET: + err = _("cannot use subquery as target of assign statement"); + break; /* * There is intentionally no default: case here, so that the @@ -3215,6 +3432,10 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_ASSIGN_TARGET: + return "ASSIGN"; + case EXPR_KIND_LET_TARGET: + return "LET"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 583bbbf232f..dc25f6dcd0e 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2656,6 +2656,8 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) err = _("set-returning functions are not allowed in column generation expressions"); break; case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_ASSIGN_TARGET: + case EXPR_KIND_LET_TARGET: errkind = true; break; diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7..244efcddf32 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -405,6 +405,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) qry->hasTargetSRFs = false; qry->hasSubLinks = pstate->p_hasSubLinks; + qry->hasSessionVariables = pstate->p_hasSessionVariables; assign_query_collations(pstate, qry); diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c938..b2f764b657f 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -38,6 +38,7 @@ #include "executor/functions.h" #include "executor/tqueue.h" #include "executor/tstoreReceiver.h" +#include "executor/svariableReceiver.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" @@ -155,6 +156,9 @@ CreateDestReceiver(CommandDest dest) case DestExplainSerialize: return CreateExplainSerializeDestReceiver(NULL); + + case DestVariable: + return CreateVariableDestReceiver(InvalidOid); } /* should never get here */ @@ -191,6 +195,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -237,6 +242,7 @@ NullCommand(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } @@ -281,6 +287,7 @@ ReadyForQuery(CommandDest dest) case DestTransientRel: case DestTupleQueue: case DestExplainSerialize: + case DestVariable: break; } } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 99a51877d34..7a508a3644f 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -49,6 +49,7 @@ #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" +#include "commands/session_variable.h" #include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -235,6 +236,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CallStmt: case T_DoStmt: + case T_LetStmt: { /* * Commands inside the DO block or the called procedure might @@ -1067,6 +1069,11 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_LetStmt: + ExecuteLetStmt(pstate, (LetStmt *) parsetree, params, + queryEnv, qc); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -2206,6 +2213,10 @@ UtilityContainsQuery(Node *parsetree) return UtilityContainsQuery(qry->utilityStmt); return qry; + case T_LetStmt: + qry = castNode(Query, ((LetStmt *) parsetree)->query); + return qry; + default: return NULL; } @@ -2404,6 +2415,10 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_SELECT; break; + case T_LetStmt: + tag = CMDTAG_LET; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -3289,6 +3304,7 @@ GetCommandLogLevel(Node *parsetree) break; case T_PLAssignStmt: + case T_LetStmt: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 9e90acedb91..d6bdae9af7d 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -37,6 +37,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" +#include "catalog/pg_variable.h" #include "commands/defrem.h" #include "commands/tablespace.h" #include "common/keywords.h" @@ -534,6 +535,7 @@ static char *generate_function_name(Oid funcid, int nargs, static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2); static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); +static char *generate_session_variable_name(Oid varid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); @@ -8729,6 +8731,14 @@ get_parameter(Param *param, deparse_context *context) return; } + /* translate paramvarid to session variable name */ + if (param->paramkind == PARAM_VARIABLE) + { + appendStringInfo(context->buf, "%s", + generate_session_variable_name(param->paramvarid)); + return; + } + /* * Alternatively, maybe it's a subplan output, which we print as a * reference to the subplan. (We could drill down into the subplan and @@ -13556,6 +13566,42 @@ generate_collation_name(Oid collid) return result; } +/* + * generate_session_variable_name + * Compute the name to display for a session variable specified by OID + * + * The result includes all necessary quoting and schema-prefixing. + */ +static char * +generate_session_variable_name(Oid varid) +{ + HeapTuple tup; + Form_pg_variable varform; + char *varname; + char *nspname; + char *result; + + tup = SearchSysCache1(VARIABLEOID, ObjectIdGetDatum(varid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for variable %u", varid); + + varform = (Form_pg_variable) GETSTRUCT(tup); + + varname = NameStr(varform->varname); + + if (!VariableIsVisible(varid)) + nspname = get_namespace_name_or_temp(varform->varnamespace); + else + nspname = NULL; + + result = quote_qualified_identifier(nspname, varname); + + ReleaseSysCache(tup); + + return result; +} + /* * Given a C string, produce a TEXT datum. * diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 6c2979d5c82..88574766861 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -58,6 +58,7 @@ #include "access/transam.h" #include "catalog/namespace.h" +#include "catalog/pg_variable.h" #include "executor/executor.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -163,6 +164,7 @@ InitPlanCache(void) CacheRegisterSyscacheCallback(AMOPOPID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNSERVEROID, PlanCacheSysCallback, (Datum) 0); CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, PlanCacheSysCallback, (Datum) 0); + CacheRegisterSyscacheCallback(VARIABLEOID, PlanCacheObjectCallback, (Datum) 0); } /* @@ -2050,17 +2052,32 @@ ScanQueryForLocks(Query *parsetree, bool acquire) /* * Recurse into sublink subqueries, too. But we already did the ones in - * the rtable and cteList. + * the rtable and cteList. We need to force a recursive call for session + * variables too, to find and lock variables used in the query (see + * ScanQueryWalker). */ - if (parsetree->hasSubLinks) + if (parsetree->hasSubLinks || + parsetree->hasSessionVariables) { query_tree_walker(parsetree, ScanQueryWalker, &acquire, QTW_IGNORE_RC_SUBQUERIES); } + + /* process session variables */ + if (OidIsValid(parsetree->resultVariable)) + { + if (acquire) + LockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, parsetree->resultVariable, + 0, AccessShareLock); + } } /* - * Walker to find sublink subqueries for ScanQueryForLocks + * Walker to find sublink subqueries or referenced session variables + * for ScanQueryForLocks */ static bool ScanQueryWalker(Node *node, bool *acquire) @@ -2075,6 +2092,20 @@ ScanQueryWalker(Node *node, bool *acquire) ScanQueryForLocks(castNode(Query, sub->subselect), *acquire); /* Fall through to process lefthand args of SubLink */ } + else if (IsA(node, Param)) + { + Param *p = (Param *) node; + + if (p->paramkind == PARAM_VARIABLE) + { + if (acquire) + LockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + else + UnlockDatabaseObject(VariableRelationId, p->paramvarid, + 0, AccessShareLock); + } + } /* * Do NOT recurse into Query nodes, because ScanQueryForLocks already @@ -2205,7 +2236,9 @@ PlanCacheRelCallback(Datum arg, Oid relid) /* * PlanCacheObjectCallback - * Syscache inval callback function for PROCOID and TYPEOID caches + * Syscache inval callback function for TYPEOID, PROCOID, NAMESPACEOID, + * OPEROID, AMOPOPID, FOREIGNSERVEROID, FOREIGNDATAWRAPPEROID and + * VARIABLEOID caches. * * Invalidate all plans mentioning the object with the specified hash value, * or all plans mentioning any member of this cache if hashvalue == 0. diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d9998..093622fbdc4 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -2026,9 +2026,13 @@ get_call_expr_arg_stable(Node *expr, int argnum) */ if (IsA(arg, Const)) return true; - if (IsA(arg, Param) && - ((Param *) arg)->paramkind == PARAM_EXTERN) - return true; + if (IsA(arg, Param)) + { + Param *p = (Param *) arg; + + if (p->paramkind == PARAM_EXTERN || p->paramkind == PARAM_VARIABLE) + return true; + } return false; } diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 78c0544e516..b3d4ef54456 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1228,8 +1228,8 @@ static const char *const sql_commands[] = { "ABORT", "ALTER", "ANALYZE", "BEGIN", "CALL", "CHECKPOINT", "CLOSE", "CLUSTER", "COMMENT", "COMMIT", "COPY", "CREATE", "DEALLOCATE", "DECLARE", "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", - "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", - "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", + "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LET", + "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", @@ -4693,6 +4693,14 @@ match_previous_words(int pattern_id, else if (TailMatches("VALUES") && !TailMatches("DEFAULT", "VALUES")) COMPLETE_WITH("("); +/* LET */ + /* If prev. word is LET suggest a list of variables */ + else if (Matches("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + /* Complete LET with "=" */ + else if (TailMatches("LET", MatchAny)) + COMPLETE_WITH("="); + /* LOCK */ /* Complete LOCK [TABLE] [ONLY] with a list of tables */ else if (Matches("LOCK")) diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index bdac0c13bec..5060b8c9490 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -174,6 +174,7 @@ extern bool SearchPathMatchesCurrentEnvironment(SearchPathMatcher *path); extern List *NamesFromList(List *names); extern Oid LookupVariable(const char *nspname, const char *varname, bool missing_ok); extern Oid LookupVariableFromNameList(List *names, bool missing_ok); +extern Oid IdentifyVariable(List *names, char **attrname, bool *not_unique, bool noerror); extern Oid get_collation_oid(List *collname, bool missing_ok); extern Oid get_conversion_oid(List *conname, bool missing_ok); diff --git a/src/include/catalog/pg_variable.h b/src/include/catalog/pg_variable.h index 3d3c6eb24b5..5b45be5afaf 100644 --- a/src/include/catalog/pg_variable.h +++ b/src/include/catalog/pg_variable.h @@ -18,6 +18,7 @@ #ifndef PG_VARIABLE_H #define PG_VARIABLE_H +#include "access/xlogdefs.h" #include "catalog/genbki.h" #include "catalog/objectaddress.h" #include "catalog/pg_variable_d.h" @@ -35,6 +36,14 @@ CATALOG(pg_variable,9222,VariableRelationId) /* OID of entry in pg_type for variable's type */ Oid vartype BKI_LOOKUP(pg_type); + /* + * Used for identity check [oid, create_lsn]. + * + * This column of the 8-byte XlogRecPtr type should be at an address that + * is divisible by 8, but before any column of type NameData. + */ + XLogRecPtr varcreate_lsn; + /* variable name */ NameData varname; diff --git a/src/include/commands/session_variable.h b/src/include/commands/session_variable.h new file mode 100644 index 00000000000..2391b9ad443 --- /dev/null +++ b/src/include/commands/session_variable.h @@ -0,0 +1,30 @@ +/*------------------------------------------------------------------------- + * + * 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 "nodes/params.h" +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" +#include "tcop/cmdtag.h" +#include "utils/queryenvironment.h" + +extern void SetSessionVariable(Oid varid, Datum value, bool isNull); +extern Datum GetSessionVariable(Oid varid, bool *isNull); + +extern void ExecuteLetStmt(ParseState *pstate, LetStmt *stmt, ParamListInfo params, + QueryEnvironment *queryEnv, QueryCompletion *qc); + +#endif diff --git a/src/include/executor/svariableReceiver.h b/src/include/executor/svariableReceiver.h new file mode 100644 index 00000000000..db44d8b94c6 --- /dev/null +++ b/src/include/executor/svariableReceiver.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * svariableReceiver.h + * prototypes for svariableReceiver.c + * + * + * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/svariableReceiver.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SVARIABLE_RECEIVER_H +#define SVARIABLE_RECEIVER_H + +#include "tcop/dest.h" + +extern DestReceiver *CreateVariableDestReceiver(Oid varid); + +#endif /* SVARIABLE_RECEIVER_H */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 5b6cadb5a6c..79c0732aad5 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -642,6 +642,16 @@ typedef struct AsyncRequest * tuples) */ } AsyncRequest; +/* ---------------- + * SessionVariableValue + * ---------------- + */ +typedef struct SessionVariableValue +{ + bool isnull; + Datum value; +} SessionVariableValue; + /* ---------------- * EState information * @@ -702,6 +712,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 3decee1a98f..e935515e5b5 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -142,6 +142,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* target variable of LET statement */ + Oid resultVariable; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -162,6 +165,8 @@ typedef struct Query bool hasRowSecurity pg_node_attr(query_jumble_ignore); /* parser has added an RTE_GROUP RTE */ bool hasGroupRTE pg_node_attr(query_jumble_ignore); + /* uses session variables */ + bool hasSessionVariables pg_node_attr(query_jumble_ignore); /* is a RETURN statement */ bool isReturn pg_node_attr(query_jumble_ignore); @@ -2149,6 +2154,18 @@ typedef struct MergeStmt ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; +/* ---------------------- + * Let Statement + * ---------------------- + */ +typedef struct LetStmt +{ + NodeTag type; + List *target; /* target variable */ + Node *query; /* source expression */ + ParseLoc location; +} LetStmt; + /* ---------------------- * Select Statement * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index c24a1fc8514..852ab05d262 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -182,6 +182,18 @@ typedef struct PlannerGlobal /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* list of used session variables */ + List *sessionVariables; + + /* Oid of session variable used like target of LET command */ + Oid resultVariable; + + /* oid of session variable used like base node for assignment indirection */ + Oid basenodeSessionVarid; + + /* true, if we do SELECT permission check on basenodeSessionVarid */ + bool basenodeSessionVarSelectCheck; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -532,6 +544,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if session variables were used */ + bool hasSessionVariables; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 658d76225e4..492b9d97e5c 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -138,6 +138,15 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + List *sessionVariables; /* OIDs for PARAM_VARIABLE Params */ + + /* + * The oid of session variable execluded from permission check. + * This session variable is used as base node of assignment indirection + * (and it is used only there). + */ + int exclSelectPermCheckVarid; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f26..3cc2f019abb 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -378,6 +378,9 @@ typedef struct Const * of the `paramid' field contain the SubLink's subLinkId, and * the low-order 16 bits contain the column number. (This type * of Param is also converted to PARAM_EXEC during planning.) + * + * PARAM_VARIABLE: The parameter is a reference to a session variable + * (paramvarid holds the variable's OID). */ typedef enum ParamKind { @@ -385,6 +388,7 @@ typedef enum ParamKind PARAM_EXEC, PARAM_SUBLINK, PARAM_MULTIEXPR, + PARAM_VARIABLE, } ParamKind; typedef struct Param @@ -397,6 +401,16 @@ typedef struct Param int32 paramtypmod pg_node_attr(query_jumble_ignore); /* OID of collation, or InvalidOid if none */ Oid paramcollid pg_node_attr(query_jumble_ignore); + /* OID of session variable if it is used */ + Oid paramvarid pg_node_attr(query_jumble_ignore); + + /* + * true if param is used as base node of assignment indirection + * (when target of LET statement is an array field or an record field). + * For this param we do not check SELECT access right, because this + * param is used just for execution of UPDATE operation. + */ + bool parambasenode; /* token location, or -1 if unknown */ ParseLoc location; } Param; diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 5a930199611..7a3b522b5ff 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -131,4 +131,6 @@ extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void record_plan_type_dependency(PlannerInfo *root, Oid typid); extern bool extract_query_dependencies_walker(Node *node, PlannerInfo *context); +extern void pull_up_has_session_variables(PlannerInfo *root); + #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 793ace526df..431167f6b43 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -257,6 +257,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD, AS_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fb..0b7b69a4159 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -82,6 +82,8 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_ASSIGN_TARGET, /* PL/pgSQL assignment target */ + EXPR_KIND_LET_TARGET, /* LET target */ } ParseExprKind; @@ -244,6 +246,7 @@ struct ParseState bool p_hasTargetSRFs; bool p_hasSubLinks; bool p_hasModifyingCTE; + bool p_hasSessionVariables; Node *p_last_srf; /* most recent set-returning func/op found */ diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index ea86954dded..22082c30008 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -186,6 +186,7 @@ PG_CMDTAG(CMDTAG_GRANT, "GRANT", true, false, false) PG_CMDTAG(CMDTAG_GRANT_ROLE, "GRANT ROLE", false, false, false) PG_CMDTAG(CMDTAG_IMPORT_FOREIGN_SCHEMA, "IMPORT FOREIGN SCHEMA", true, false, false) PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true) +PG_CMDTAG(CMDTAG_LET, "LET", false, false, false) PG_CMDTAG(CMDTAG_LISTEN, "LISTEN", false, false, false) PG_CMDTAG(CMDTAG_LOAD, "LOAD", false, false, false) PG_CMDTAG(CMDTAG_LOCK_TABLE, "LOCK TABLE", false, false, false) diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c..6ce3ea0e617 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -97,6 +97,7 @@ typedef enum DestTransientRel, /* results sent to transient relation */ DestTupleQueue, /* results sent to tuple queue */ DestExplainSerialize, /* results are serialized and discarded */ + DestVariable, /* results sent to session variable */ } CommandDest; /* ---------------- diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e..1cff5efa6f7 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -8264,7 +8264,8 @@ exec_is_simple_query(PLpgSQL_expr *expr) query->sortClause || query->limitOffset || query->limitCount || - query->setOperations) + query->setOperations || + query->hasSessionVariables) return false; /* diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out index c36660ce6d3..f22467186e3 100644 --- a/src/test/regress/expected/session_variables.out +++ b/src/test/regress/expected/session_variables.out @@ -431,3 +431,1235 @@ DROP VARIABLE public.var1; DROP ROLE regress_variable_r1; DROP ROLE regress_variable_r2; DROP ROLE regress_variable_owner; +-- check access rights +CREATE ROLE regress_noowner; +CREATE VARIABLE var1 AS int; +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; +LET var1 = 10; +-- should be ok +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +-- should fail +SET ROLE TO regress_noowner; +SELECT var1; +ERROR: permission denied for session variable var1 +SELECT sqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: SQL function "sqlfx" statement 1 +SELECT plpgsqlfx(20); +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "$1 + var1" +PL/pgSQL function plpgsqlfx(integer) line 1 at RETURN +-- should be ok +SELECT sqlfx_sd(20); + sqlfx_sd +---------- + 30 +(1 row) + +SELECT plpgsqlfx_sd(20); + plpgsqlfx_sd +-------------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; +-- should be ok +SET ROLE TO regress_noowner; +SELECT var1; + var1 +------ + 10 +(1 row) + +SELECT sqlfx(20); + sqlfx +------- + 30 +(1 row) + +SELECT plpgsqlfx(20); + plpgsqlfx +----------- + 30 +(1 row) + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); +DROP ROLE regress_noowner; +-- use variables inside views +CREATE VARIABLE var1 AS numeric; +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- start a new session +\c +SELECT * FROM test_view; + result +-------- + 0 + 0 +(2 rows) + +LET var1 = 3.14; +SELECT * FROM test_view; + result +-------- + 4.14 + 5.14 +(2 rows) + +-- should fail, dependency +DROP VARIABLE var1; +ERROR: cannot drop session variable var1 because other objects depend on it +DETAIL: view test_view depends on session variable var1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- should be ok +DROP VARIABLE var1 CASCADE; +NOTICE: drop cascades to view test_view +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; +LET var1 = 'str1'; +LET var2 = 'str2'; +SELECT sqlfx1(sqlfx2('Hello')); + sqlfx1 +------------------- + str1, str2, Hello +(1 row) + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + QUERY PLAN +------------------------------------------------------ + Result + Output: sqlfx1(sqlfx2('Hello'::character varying)) +(2 rows) + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; +CREATE VARIABLE var2 AS numeric; +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION plpgsqlfx2(numeric) +RETURNS void AS $$ BEGIN LET var2 = $1; END $$ LANGUAGE plpgsql; +SET plan_cache_mode TO force_generic_plan; +LET var1 = 3.14; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 3.14 +(1 row) + +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 6.28 +(1 row) + +SELECT plpgsqlfx2(10.0); + plpgsqlfx2 +------------ + +(1 row) + +SELECT var2; + var2 +------ + 10.0 +(1 row) + +DROP VARIABLE var1; +DROP VARIABLE var2; +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; +CREATE VARIABLE var2 AS numeric; +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 9.42 +(1 row) + +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + plpgsqlfx +----------- + 12.56 +(1 row) + +SELECT plpgsqlfx2(10.0); + plpgsqlfx2 +------------ + +(1 row) + +SELECT var2; + var2 +------ + 10.0 +(1 row) + +DROP VARIABLE var1; +DROP VARIABLE var2; +DROP FUNCTION plpgsqlfx(); +DROP FUNCTION plpgsqlfx2(); +ERROR: function plpgsqlfx2() does not exist +-- dependency on column type +CREATE VARIABLE var1 AS int; +CREATE TABLE testvar(a int, b int, c int); +INSERT INTO testvar VALUES(10,20,30); +ALTER TABLE testvar DROP COLUMN a; +ALTER TABLE testvar DROP COLUMN b; +CREATE FUNCTION plpgsqlfx3() +RETURNS void AS $$ +BEGIN + LET var1 = (SELECT * FROM testvar); + RAISE NOTICE '%', var1; +END +$$ LANGUAGE plpgsql; +-- should be ok +SELECT plpgsqlfx3(); +NOTICE: 30 + plpgsqlfx3 +------------ + +(1 row) + +ALTER TABLE testvar ALTER COLUMN c TYPE numeric; +-- should be ok +SELECT plpgsqlfx3(); +NOTICE: 30 + plpgsqlfx3 +------------ + +(1 row) + +DROP FUNCTION plpgsqlfx3(); +DROP TABLE testvar; +DROP VARIABLE var1; +SET plan_cache_mode TO DEFAULT; +-- repeated execution should not crash +CREATE VARIABLE var1 int; +CREATE TABLE testvar(a int); +INSERT INTO testvar VALUES(1); +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a FROM testvar); + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: 1 +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a + var1 FROM testvar); + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: 10 +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a FROM testvar) + var1; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: 10 +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = i; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: 10 +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = i + var1; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: 55 +DROP VARIABLE var1; +DROP TABLE testvar; +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; +NOTICE: result array: {1,2,3,4,5,6,7,8,9,10} +DROP VARIABLE var1; +DROP VARIABLE var2; +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; +-- should not crash (but is not supported yet) +CALL p(v); +ERROR: session variable cannot be used as an argument +DO $$ BEGIN CALL p(v); END $$; +ERROR: session variable cannot be used as an argument +CONTEXT: SQL statement "CALL p(v)" +PL/pgSQL function inline_code_block line 1 at CALL +DROP PROCEDURE p(int); +DROP VARIABLE v; +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; +-- should fail +EXECUTE ptest(v); +ERROR: session variable cannot be used as an argument +DEALLOCATE ptest; +DROP VARIABLE v; +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; +-- should fail +LET var1 = pi(); +ERROR: session variable "var1" doesn't exist +LINE 1: LET var1 = pi(); + ^ +SELECT var1; +ERROR: column "var1" does not exist +LINE 1: SELECT var1; + ^ +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + var1 +------------------ + 3.14159265358979 +(1 row) + +SET search_path TO svartest; +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + var1 +------------------ + 13.1415926535898 +(1 row) + +RESET search_path; +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to session variable svartest.var1 +CREATE VARIABLE var1 AS text; +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; +SELECT var1; + var1 +------- + hello +(1 row) + +DROP VARIABLE var1; +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; +-- should fail +SELECT var1; +ERROR: domain int_domain does not allow null values +-- should be ok +LET var1 = 1000; +SELECT var1; + var1 +------ + 1000 +(1 row) + +-- should fail +LET var1 = 10; +ERROR: value for domain int_domain violates check constraint "int_domain_check" +-- should fail +LET var1 = NULL; +ERROR: domain int_domain does not allow null values +-- note - domain defaults are not supported yet (like PLpgSQL) +DROP VARIABLE var1; +DROP DOMAIN int_domain; +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + var1 +---------------------------------------- + {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +(1 row) + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; +NOTICE: {"{[2,8),[11,14)}","{[5,8),[12,100)}"} +DROP VARIABLE var1; +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + var1 +------ + 100 +(1 row) + +SET search_path to public, svartest; +SELECT var1; + var1 +------ + 100 +(1 row) + +DROP SCHEMA svartest CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table foo +drop cascades to session variable var1 +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +LET var2[var1] = 0; +SELECT var2; + var2 +----------- + [2:2]={0} +(1 row) + +DROP VARIABLE var1, var2; +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; +LET var1 = 2; +LET var2 = '{}'::int[]; +SELECT var2; + var2 +------ + {} +(1 row) + +DROP VARIABLE var1, var2; +-- the LET statement should be disallowed in CTE +CREATE VARIABLE var1 AS int; +WITH x AS (LET var1 = 100) SELECT * FROM x; +ERROR: syntax error at or near "LET" +LINE 1: WITH x AS (LET var1 = 100) SELECT * FROM x; + ^ +-- should be ok +LET var1 = generate_series(1, 1); +-- should fail +LET var1 = generate_series(1, 2); +ERROR: expression returned more than one row +LET var1 = generate_series(1, 0); +ERROR: expression returned no rows +DROP VARIABLE var1; +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); +SELECT v1; + v1 +------------ + (1,2,3.00) +(1 row) + +SELECT v2; + v2 +--------------- + (10,20,31.40) +(1 row) + +SELECT (v1).*; + x | y | z +---+---+------ + 1 | 2 | 3.00 +(1 row) + +SELECT (v2).*; + x | y | z +----+----+------- + 10 | 20 | 31.40 +(1 row) + +SELECT v1.x + v1.z; + ?column? +---------- + 4.00 +(1 row) + +SELECT v2.x + v2.z; + ?column? +---------- + 41.40 +(1 row) + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should fail +SELECT v2.x; +ERROR: permission denied for session variable v2 +SET ROLE TO DEFAULT; +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; +-- should fail, wrong identifier +LET nodb.noschema.novar.nofield.nosubfield = 10; +ERROR: improper qualified name (too many dotted names): nodb.noschema.novar.nofield.nosubfield +LET nodb.noschema.novar.nofield = 10; +ERROR: cross-database references are not implemented: nodb.noschema.novar.nofield +LET nodb.noschema.novar = 10; +ERROR: cross-database references are not implemented: nodb.noschema.novar +CREATE TYPE t1 AS (a int, b numeric, c text); +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; + v1 +---------------------------- + (1,3.14159265358979,hello) +(1 row) + +LET v1.b = 10.2222; +SELECT v1; + v1 +------------------- + (1,10.2222,hello) +(1 row) + +-- should fail, attribute doesn't exist +LET v1.x = 10; +ERROR: cannot assign to field "x" of column "v1" because there is no such column in data type t1 +LINE 1: LET v1.x = 10; + ^ +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; +ERROR: assignment expression returned 3 columns +LINE 1: LET v1 = (NULL::t1).*; + ^ +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + v1 +------------- + (1,10.2222) +(1 row) + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + v1 +-------------- + (1,10.2222,) +(1 row) + +LET v1 = (10, 10.3, 20); +SELECT v1; + v1 +-------------- + (10,10.3,20) +(1 row) + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + v1 +--------- + (10,20) +(1 row) + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; +ERROR: cannot alter type "t1" because session variable "public.v1" uses it +DROP VARIABLE v1; +DROP TYPE t1; +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + var1 +---------------------------------- + (10,3.14159265358979,05-26-2023) +(1 row) + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; +ERROR: cannot alter table "svar_test" because session variable "public.var1" uses it +-- should fail +DROP TABLE svar_test; +ERROR: cannot drop table svar_test because other objects depend on it +DETAIL: session variable var1 depends on type svar_test +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VARIABLE var1; +DROP TABLE svar_test; +CREATE TYPE vartest_t1 AS (a int, b int); +CREATE VARIABLE var1 AS vartest_t1; +CREATE TABLE vartesttab (xcol int); +CREATE ROLE regress_var_test_role; +GRANT UPDATE ON VARIABLE var1 TO regress_var_test_role; +GRANT SELECT ON TABLE vartesttab TO regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should be ok +LET var1 = (10, 20); +LET var1.a = 30; +DO $$ +BEGIN + LET var1 = (100, 100); + LET var1.a = 1000; +END; +$$; +-- should fail +SELECT var1.a; +ERROR: permission denied for session variable var1 +SELECT var1; +ERROR: permission denied for session variable var1 +LET var1.a = var1.a + 10; +ERROR: permission denied for session variable var1 +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); +ERROR: permission denied for session variable var1 +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "var1" +PL/pgSQL function inline_code_block line 1 at RAISE +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "var1.a" +PL/pgSQL function inline_code_block line 1 at RAISE +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: SQL statement "LET var1.a = var1.a + 10" +PL/pgSQL function inline_code_block line 1 at SQL statement +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should be ok +SELECT var1.a; + a +------ + 1000 +(1 row) + +SELECT var1; + var1 +------------ + (1000,100) +(1 row) + +LET var1.a = var1.a + 10; +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +NOTICE: (0,100) +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +NOTICE: 0 +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; +SET ROLE TO DEFAULT; +REVOKE SELECT ON VARIABLE var1 FROM regress_var_test_role; +SET ROLE TO regress_var_test_role; +-- should fail again +SELECT var1.a; +ERROR: permission denied for session variable var1 +SELECT var1; +ERROR: permission denied for session variable var1 +LET var1.a = var1.a + 10; +ERROR: permission denied for session variable var1 +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); +ERROR: permission denied for session variable var1 +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "var1" +PL/pgSQL function inline_code_block line 1 at RAISE +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: PL/pgSQL expression "var1.a" +PL/pgSQL function inline_code_block line 1 at RAISE +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; +ERROR: permission denied for session variable var1 +CONTEXT: SQL statement "LET var1.a = var1.a + 10" +PL/pgSQL function inline_code_block line 1 at SQL statement +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP TABLE vartesttab; +DROP ROLE regress_var_test_role; +DROP TYPE vartest_t1; +CREATE TYPE vartest_t1 AS (a int, b int); +CREATE VARIABLE var1 AS text; +CREATE VARIABLE var2 AS vartest_t1; +-- should fail +SELECT var1.a; +ERROR: variable "public.var1" is of type "text", which is not a composite type +LINE 1: SELECT var1.a; + ^ +SELECT var2.c; +ERROR: could not identify column "c" in variable "public.var2" +LINE 1: SELECT var2.c; + ^ +DROP VARIABLE var1; +DROP VARIABLE var2; +DROP TYPE vartest_t1; +CREATE VARIABLE var1 AS int; +LET var1 = 1; +LET var1 = '1'; +LET var1 = 10.1; +-- should fail +LET var1 = '1'::jsonb; +ERROR: variable "public.var1" is of type integer, but expression is of type jsonb +LINE 1: LET var1 = '1'::jsonb; + ^ +HINT: You will need to rewrite or cast the expression. +-- should be ok +LET var1 = '1'::jsonb::int; +-- should fail +LET var1.x = 10; +ERROR: cannot assign to field "x" of session variable "public.var1" because its type integer is not a composite type +LINE 1: LET var1.x = 10; + ^ +DROP VARIABLE var1; +CREATE SCHEMA vartest; +CREATE TYPE vartesttype AS (vartest int); +CREATE VARIABLE vartest.vartest AS vartesttype; +SET SEARCH_PATH TO vartest; +-- should fail +LET vartest.vartest = 1; +ERROR: target "vartest.vartest" of LET command is ambiguous +LINE 1: LET vartest.vartest = 1; + ^ +DROP VARIABLE vartest.vartest; +DROP TYPE vartesttype; +ERROR: type "vartesttype" does not exist +DROP SCHEMA vartest; +SET SEARCH_PATH TO DEFAULT; +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + var1 +------------ + {10.1,2.1} +(1 row) + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; +ERROR: set-returning functions are not allowed in LET +LINE 1: LET var1[generate_series(1,3)] = 100; + ^ +DROP VARIABLE var1; +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; + var1 +-------------------- + (10.2,"{0.0,0.0}") +(1 row) + +LET var1.b[1] = 10.3; +SELECT var1; + var1 +--------------------- + (10.2,"{10.3,0.0}") +(1 row) + +DROP VARIABLE var1; +DROP TYPE t1; +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + count +------- + 100 +(1 row) + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + QUERY PLAN +----------------------------------- + Aggregate + -> Seq Scan on svar_test + Filter: ((a % 10) = zero) +(3 rows) + +LET zero = (SELECT count(*) FROM svar_test); +-- result should be 1000 +SELECT zero; + zero +------ + 1000 +(1 row) + +DROP VARIABLE zero; +DROP TABLE svar_test; +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO on; +SELECT * FROM var1view; + result +-------- + 10 +(1 row) + +SET debug_parallel_query TO off; +DROP VIEW var1view; +DROP VARIABLE var1; +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; + id | v +----+----- + 1 | 100 +(1 row) + +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+----- + 1 | 200 +(1 row) + +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + id | v +----+--- +(0 rows) + +DROP TABLE svar_test; +DROP VARIABLE varid; +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + relname +---------- + pg_class +(1 row) + +DROP VARIABLE var1; +CREATE TABLE xxtab(avar int); +INSERT INTO xxtab VALUES(333); +CREATE TYPE xxtype AS (avar int); +CREATE VARIABLE xxtab AS xxtype; +INSERT INTO xxtab VALUES(10); +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +CREATE VARIABLE public.avar AS int; +-- should be ok, see the table +SELECT avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +-- should be ok +SELECT public.avar FROM xxtab; + avar +------ + + +(2 rows) + +DROP VARIABLE xxtab; +SELECT xxtab.avar FROM xxtab; + avar +------ + 333 + 10 +(2 rows) + +DROP VARIABLE public.avar; +DROP TYPE xxtype; +DROP TABLE xxtab; +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; +CREATE TABLE public.svar(a int, b int); +INSERT INTO public.svar VALUES(10, 20); +LET public.svar = (100, 200, 300); +-- should be ok +-- show table +SELECT * FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +SELECT svar.a FROM public.svar; + a +---- + 10 +(1 row) + +SELECT svar.* FROM public.svar; + a | b +----+---- + 10 | 20 +(1 row) + +-- show variable +SELECT public.svar; + svar +--------------- + (100,200,300) +(1 row) + +SELECT public.svar.c; + c +----- + 300 +(1 row) + +SELECT (public.svar).*; + a | b | c +-----+-----+----- + 100 | 200 | 300 +(1 row) + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; +ERROR: column svar.c does not exist +LINE 1: SELECT public.svar.c FROM public.svar; + ^ +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + c +----- + 300 +(1 row) + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; +-- The sequence ident.* cannot be used by reference to session variable, +-- and in this case, there is not collision +CREATE SCHEMA vartest; +SET SEARCH_PATH TO vartest; +CREATE TABLE vartest_foo(a int, b int); +INSERT INTO vartest_foo VALUES(10,20), (30,40); +CREATE VARIABLE var1 AS vartest_foo; +LET var1 = (100,200); +-- should to fail +SELECT var1.* FROM vartest_foo; +ERROR: missing FROM-clause entry for table "var1" +LINE 1: SELECT var1.* FROM vartest_foo; + ^ +-- should be ok +SELECT var1.* FROM vartest_foo var1; + a | b +----+---- + 10 | 20 + 30 | 40 +(2 rows) + +-- should to fail +SELECT count(var1.*) FROM vartest_foo; +ERROR: missing FROM-clause entry for table "var1" +LINE 1: SELECT count(var1.*) FROM vartest_foo; + ^ +-- should be ok +SELECT count(var1.*) FROM vartest_foo var1; + count +------- + 2 +(1 row) + +SET SEARCH_PATH TO DEFAULT; +DROP SCHEMA vartest CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table vartest.vartest_foo +drop cascades to session variable vartest.var1 +CREATE TYPE ab AS (a integer, b integer); +CREATE VARIABLE v_ab AS ab; +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +CREATE SCHEMA v_ab; +CREATE VARIABLE v_ab.a AS integer; +-- we should see table +SELECT v_ab.a FROM v_ab; + a +---- + 10 +(1 row) + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; +SET search_path TO public; +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); +-- should be ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; +-- should be still ok +SELECT xxx_am; + xxx_am +-------- + (10) +(1 row) + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; +ERROR: session variable reference "xxx_am.b" is ambiguous +LINE 1: SELECT xxx_am.b; + ^ +-- enhanced references should be ok +SELECT public.xxx_am.b; + b +---- + 10 +(1 row) + +SELECT :"DBNAME".xxx_am.b; + b +---- + 20 +(1 row) + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); +-- we should see table +SELECT xxx_am.b FROM xxx_am; + b +---- + 10 +(1 row) + +SELECT x.b FROM xxx_am x; + b +---- + 10 +(1 row) + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; +CREATE SCHEMA :"DBNAME"; +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; +SET search_path TO :"DBNAME"; +-- should be ambiguous +SELECT :"DBNAME".b; +ERROR: session variable reference "regression.b" is ambiguous +LINE 1: SELECT "regression".b; + ^ +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; +ERROR: session variable reference "regression.regression.b" is ambiguous +LINE 1: SELECT "regression"."regression".b; + ^ +CREATE TABLE :"DBNAME"(b int); +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + b +--- +(0 rows) + +DROP TABLE :"DBNAME"; +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; +RESET search_path; diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql index eeb0f94ca3c..e72360d5254 100644 --- a/src/test/regress/sql/session_variables.sql +++ b/src/test/regress/sql/session_variables.sql @@ -295,3 +295,886 @@ DROP ROLE regress_variable_r1; DROP ROLE regress_variable_r2; DROP ROLE regress_variable_owner; + +-- check access rights +CREATE ROLE regress_noowner; + +CREATE VARIABLE var1 AS int; + +CREATE OR REPLACE FUNCTION sqlfx(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx_sd(int) +RETURNS int AS $$ SELECT $1 + var1 $$ LANGUAGE sql SECURITY DEFINER; + +CREATE OR REPLACE FUNCTION plpgsqlfx(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION plpgsqlfx_sd(int) +RETURNS int AS $$ BEGIN RETURN $1 + var1; END $$ LANGUAGE plpgsql SECURITY DEFINER; + +LET var1 = 10; +-- should be ok +SELECT var1; +SELECT sqlfx(20); +SELECT sqlfx_sd(20); +SELECT plpgsqlfx(20); +SELECT plpgsqlfx_sd(20); + +-- should fail +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +-- should be ok +SELECT sqlfx_sd(20); +SELECT plpgsqlfx_sd(20); + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_noowner; + +-- should be ok +SET ROLE TO regress_noowner; + +SELECT var1; +SELECT sqlfx(20); +SELECT plpgsqlfx(20); + +SET ROLE TO DEFAULT; +DROP VARIABLE var1; +DROP FUNCTION sqlfx(int); +DROP FUNCTION plpgsqlfx(int); +DROP FUNCTION sqlfx_sd(int); +DROP FUNCTION plpgsqlfx_sd(int); + +DROP ROLE regress_noowner; + +-- use variables inside views +CREATE VARIABLE var1 AS numeric; + +-- use variables in views +CREATE VIEW test_view AS SELECT COALESCE(var1 + v, 0) AS result FROM generate_series(1,2) g(v); +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- start a new session +\c + +SELECT * FROM test_view; +LET var1 = 3.14; +SELECT * FROM test_view; + +-- should fail, dependency +DROP VARIABLE var1; + +-- should be ok +DROP VARIABLE var1 CASCADE; + +CREATE VARIABLE var1 text; +CREATE VARIABLE var2 text; + +-- use variables in SQL functions +CREATE OR REPLACE FUNCTION sqlfx1(varchar) +RETURNS varchar AS $$ SELECT var1 || ', ' || $1 $$ LANGUAGE sql; + +CREATE OR REPLACE FUNCTION sqlfx2( varchar) +RETURNS varchar AS $$ SELECT var2 || ', ' || $1 $$ LANGUAGE sql; + +LET var1 = 'str1'; +LET var2 = 'str2'; + +SELECT sqlfx1(sqlfx2('Hello')); + +-- inlining is blocked +EXPLAIN (COSTS OFF, VERBOSE) SELECT sqlfx1(sqlfx2('Hello')); + +DROP FUNCTION sqlfx1(varchar); +DROP FUNCTION sqlfx2(varchar); +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- access from cached plans should work +CREATE VARIABLE var1 AS numeric; +CREATE VARIABLE var2 AS numeric; + +CREATE OR REPLACE FUNCTION plpgsqlfx() +RETURNS numeric AS $$ BEGIN RETURN var1; END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION plpgsqlfx2(numeric) +RETURNS void AS $$ BEGIN LET var2 = $1; END $$ LANGUAGE plpgsql; + +SET plan_cache_mode TO force_generic_plan; + +LET var1 = 3.14; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 2; +SELECT plpgsqlfx(); + +SELECT plpgsqlfx2(10.0); +SELECT var2; + +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- dependency (plan invalidation) should work +CREATE VARIABLE var1 AS numeric; +CREATE VARIABLE var2 AS numeric; + +LET var1 = 3.14 * 3; +SELECT plpgsqlfx(); +LET var1 = 3.14 * 4; +SELECT plpgsqlfx(); + +SELECT plpgsqlfx2(10.0); +SELECT var2; + +DROP VARIABLE var1; +DROP VARIABLE var2; +DROP FUNCTION plpgsqlfx(); +DROP FUNCTION plpgsqlfx2(); + +-- dependency on column type +CREATE VARIABLE var1 AS int; +CREATE TABLE testvar(a int, b int, c int); +INSERT INTO testvar VALUES(10,20,30); + +ALTER TABLE testvar DROP COLUMN a; +ALTER TABLE testvar DROP COLUMN b; + +CREATE FUNCTION plpgsqlfx3() +RETURNS void AS $$ +BEGIN + LET var1 = (SELECT * FROM testvar); + RAISE NOTICE '%', var1; +END +$$ LANGUAGE plpgsql; + +-- should be ok +SELECT plpgsqlfx3(); + +ALTER TABLE testvar ALTER COLUMN c TYPE numeric; + +-- should be ok +SELECT plpgsqlfx3(); + +DROP FUNCTION plpgsqlfx3(); +DROP TABLE testvar; +DROP VARIABLE var1; + +SET plan_cache_mode TO DEFAULT; + +-- repeated execution should not crash +CREATE VARIABLE var1 int; +CREATE TABLE testvar(a int); +INSERT INTO testvar VALUES(1); + +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a FROM testvar); + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; + +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a + var1 FROM testvar); + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; + +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = (SELECT a FROM testvar) + var1; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; + +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = i; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; + +DO $$ +BEGIN + LET var1 = 0; + FOR i IN 1..10 + LOOP + LET var1 = i + var1; + END LOOP; + RAISE NOTICE '%', var1; +END; +$$; + +DROP VARIABLE var1; +DROP TABLE testvar; + +-- usage LET statement in plpgsql should work +CREATE VARIABLE var1 int; +CREATE VARIABLE var2 numeric[]; + +DO $$ +BEGIN + LET var2 = '{}'::int[]; + FOR i IN 1..10 + LOOP + LET var1 = i; + LET var2[var1] = i; + END LOOP; + RAISE NOTICE 'result array: %', var2; +END; +$$; + +DROP VARIABLE var1; +DROP VARIABLE var2; + +-- CALL statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; + +CREATE PROCEDURE p(arg int) AS $$ BEGIN RAISE NOTICE '%', arg; END $$ LANGUAGE plpgsql; + +-- should not crash (but is not supported yet) +CALL p(v); + +DO $$ BEGIN CALL p(v); END $$; + +DROP PROCEDURE p(int); +DROP VARIABLE v; + +-- EXECUTE statement is not supported yet +-- requires direct access to session variable from expression executor +CREATE VARIABLE v int; +LET v = 20; +PREPARE ptest(int) AS SELECT $1; + +-- should fail +EXECUTE ptest(v); + +DEALLOCATE ptest; +DROP VARIABLE v; + +-- test search path +CREATE SCHEMA svartest; +CREATE VARIABLE svartest.var1 AS numeric; + +-- should fail +LET var1 = pi(); +SELECT var1; + +-- should be ok +LET svartest.var1 = pi(); +SELECT svartest.var1; + +SET search_path TO svartest; + +-- should be ok +LET var1 = pi() + 10; +SELECT var1; + +RESET search_path; +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS text; + +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET var1 = 'hello'; +COMMIT; + +SELECT var1; + +DROP VARIABLE var1; + +-- test of domains +CREATE DOMAIN int_domain AS int NOT NULL CHECK (VALUE > 100); +CREATE VARIABLE var1 AS int_domain; + +-- should fail +SELECT var1; + +-- should be ok +LET var1 = 1000; +SELECT var1; + +-- should fail +LET var1 = 10; + +-- should fail +LET var1 = NULL; + +-- note - domain defaults are not supported yet (like PLpgSQL) + +DROP VARIABLE var1; +DROP DOMAIN int_domain; + +-- the expression should remain "unknown" +CREATE VARIABLE var1 AS int4multirange[]; +-- should be ok +LET var1 = NULL; +LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; +LET var1[2] = '{[5,8),[12,100)}'; +SELECT var1; + +--It should work in plpgsql too +DO $$ +BEGIN + LET var1 = NULL; + LET var1 = '{"{[2,8),[11,14)}","{[5,8),[12,14)}"}'; + LET var1[2] = '{[5,8),[12,100)}'; + + RAISE NOTICE '%', var1; +END; +$$; + +DROP VARIABLE var1; + +CREATE SCHEMA svartest CREATE VARIABLE var1 AS int CREATE TABLE foo(a int); +LET svartest.var1 = 100; +SELECT svartest.var1; + +SET search_path to public, svartest; + +SELECT var1; + +DROP SCHEMA svartest CASCADE; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +LET var2[var1] = 0; + +SELECT var2; + +DROP VARIABLE var1, var2; + +CREATE VARIABLE var1 AS int; +CREATE VARIABLE var2 AS int[]; + +LET var1 = 2; +LET var2 = '{}'::int[]; + +SELECT var2; + +DROP VARIABLE var1, var2; + +-- the LET statement should be disallowed in CTE +CREATE VARIABLE var1 AS int; +WITH x AS (LET var1 = 100) SELECT * FROM x; + +-- should be ok +LET var1 = generate_series(1, 1); + +-- should fail +LET var1 = generate_series(1, 2); +LET var1 = generate_series(1, 0); + +DROP VARIABLE var1; + +-- composite variables +CREATE TYPE sv_xyz AS (x int, y int, z numeric(10,2)); + +CREATE VARIABLE v1 AS sv_xyz; +CREATE VARIABLE v2 AS sv_xyz; + +LET v1 = (1, 2, 3.14); +LET v2 = (10, 20, 3.14 * 10); + +-- should work too - there are prepared casts +LET v1 = (1, 2, 3); + +SELECT v1; +SELECT v2; +SELECT (v1).*; +SELECT (v2).*; + +SELECT v1.x + v1.z; +SELECT v2.x + v2.z; + +-- access to composite fields should be safe too +CREATE ROLE regress_var_test_role; + +SET ROLE TO regress_var_test_role; + +-- should fail +SELECT v2.x; + +SET ROLE TO DEFAULT; + +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP TYPE sv_xyz; +DROP ROLE regress_var_test_role; + +-- should fail, wrong identifier +LET nodb.noschema.novar.nofield.nosubfield = 10; +LET nodb.noschema.novar.nofield = 10; +LET nodb.noschema.novar = 10; + +CREATE TYPE t1 AS (a int, b numeric, c text); + +CREATE VARIABLE v1 AS t1; +LET v1 = (1, pi(), 'hello'); +SELECT v1; +LET v1.b = 10.2222; +SELECT v1; + +-- should fail, attribute doesn't exist +LET v1.x = 10; + +-- should fail, don't allow multi column query +LET v1 = (NULL::t1).*; + +-- allow DROP or ADD ATTRIBUTE on composite types +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE c; +SELECT v1; + +-- should be ok +ALTER TYPE t1 ADD ATTRIBUTE c int; +SELECT v1; + +LET v1 = (10, 10.3, 20); +SELECT v1; + +-- should be ok +ALTER TYPE t1 DROP ATTRIBUTE b; +SELECT v1; + +-- should fail, disallow data type change +ALTER TYPE t1 ALTER ATTRIBUTE c TYPE int; + +DROP VARIABLE v1; +DROP TYPE t1; + +-- the table type can be used as composite type too +CREATE TABLE svar_test(a int, b numeric, c date); +CREATE VARIABLE var1 AS svar_test; + +LET var1 = (10, pi(), '2023-05-26'); +SELECT var1; + +-- should fail due dependency +ALTER TABLE svar_test ALTER COLUMN a TYPE text; + +-- should fail +DROP TABLE svar_test; + +DROP VARIABLE var1; +DROP TABLE svar_test; + +CREATE TYPE vartest_t1 AS (a int, b int); +CREATE VARIABLE var1 AS vartest_t1; +CREATE TABLE vartesttab (xcol int); + +CREATE ROLE regress_var_test_role; + +GRANT UPDATE ON VARIABLE var1 TO regress_var_test_role; +GRANT SELECT ON TABLE vartesttab TO regress_var_test_role; + +SET ROLE TO regress_var_test_role; + +-- should be ok +LET var1 = (10, 20); +LET var1.a = 30; + +DO $$ +BEGIN + LET var1 = (100, 100); + LET var1.a = 1000; +END; +$$; + +-- should fail +SELECT var1.a; +SELECT var1; +LET var1.a = var1.a + 10; +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); + +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; + +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO regress_var_test_role; +SET ROLE TO regress_var_test_role; + +-- should be ok +SELECT var1.a; +SELECT var1; +LET var1.a = var1.a + 10; +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); + +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; + +SET ROLE TO DEFAULT; +REVOKE SELECT ON VARIABLE var1 FROM regress_var_test_role; +SET ROLE TO regress_var_test_role; + +-- should fail again +SELECT var1.a; +SELECT var1; +LET var1.a = var1.a + 10; +LET var1.a = (SELECT * FROM (SELECT count(*) FROM vartesttab WHERE xcol = var1.a + 10)); + +DO $$ BEGIN RAISE NOTICE '%', var1; END $$; +DO $$ BEGIN RAISE NOTICE '%', var1.a; END $$; +DO $$ BEGIN LET var1.a = var1.a + 10; END $$; + +SET ROLE TO DEFAULT; + +DROP VARIABLE var1; +DROP TABLE vartesttab; +DROP ROLE regress_var_test_role; +DROP TYPE vartest_t1; + +CREATE TYPE vartest_t1 AS (a int, b int); + +CREATE VARIABLE var1 AS text; +CREATE VARIABLE var2 AS vartest_t1; + +-- should fail +SELECT var1.a; +SELECT var2.c; + +DROP VARIABLE var1; +DROP VARIABLE var2; +DROP TYPE vartest_t1; + +CREATE VARIABLE var1 AS int; + +LET var1 = 1; +LET var1 = '1'; +LET var1 = 10.1; + +-- should fail +LET var1 = '1'::jsonb; + +-- should be ok +LET var1 = '1'::jsonb::int; + +-- should fail +LET var1.x = 10; + +DROP VARIABLE var1; + +CREATE SCHEMA vartest; +CREATE TYPE vartesttype AS (vartest int); +CREATE VARIABLE vartest.vartest AS vartesttype; +SET SEARCH_PATH TO vartest; + +-- should fail +LET vartest.vartest = 1; + +DROP VARIABLE vartest.vartest; +DROP TYPE vartesttype; +DROP SCHEMA vartest; +SET SEARCH_PATH TO DEFAULT; + +-- arrays are supported +CREATE VARIABLE var1 AS numeric[]; +LET var1 = ARRAY[1.1,2.1]; +LET var1[1] = 10.1; +SELECT var1; + +-- LET target doesn't allow srf, should fail +LET var1[generate_series(1,3)] = 100; + +DROP VARIABLE var1; + +-- arrays inside composite +CREATE TYPE t1 AS (a numeric, b numeric[]); +CREATE VARIABLE var1 AS t1; +LET var1 = (10.1, ARRAY[0.0, 0.0]); +LET var1.a = 10.2; +SELECT var1; +LET var1.b[1] = 10.3; +SELECT var1; + +DROP VARIABLE var1; +DROP TYPE t1; + +-- Encourage use of parallel plans +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET max_parallel_workers_per_gather = 2; + +-- test on query with workers +CREATE TABLE svar_test(a int); +INSERT INTO svar_test SELECT * FROM generate_series(1,1000); +ANALYZE svar_test; +CREATE VARIABLE zero int; +LET zero = 0; + +-- result should be 100 +SELECT count(*) FROM svar_test WHERE a%10 = zero; + +-- parallel execution is not supported yet +EXPLAIN (COSTS OFF) SELECT count(*) FROM svar_test WHERE a%10 = zero; + +LET zero = (SELECT count(*) FROM svar_test); + +-- result should be 1000 +SELECT zero; + +DROP VARIABLE zero; +DROP TABLE svar_test; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- the result of view should be same in parallel mode too +CREATE VARIABLE var1 AS int; +LET var1 = 10; + +CREATE VIEW var1view AS SELECT COALESCE(var1, 0) AS result; + +SELECT * FROM var1view; + +SET debug_parallel_query TO on; + +SELECT * FROM var1view; + +SET debug_parallel_query TO off; + +DROP VIEW var1view; +DROP VARIABLE var1; + +CREATE VARIABLE varid int; +CREATE TABLE svar_test(id int, v int); + +LET varid = 1; +INSERT INTO svar_test VALUES(varid, 100); +SELECT * FROM svar_test; +UPDATE svar_test SET v = 200 WHERE id = varid; +SELECT * FROM svar_test; +DELETE FROM svar_test WHERE id = varid; +SELECT * FROM svar_test; + +DROP TABLE svar_test; +DROP VARIABLE varid; + +-- visibility check +-- variables should be shadowed always +CREATE VARIABLE var1 AS text; +SELECT var1.relname FROM pg_class var1 WHERE var1.relname = 'pg_class'; + +DROP VARIABLE var1; + +CREATE TABLE xxtab(avar int); + +INSERT INTO xxtab VALUES(333); + +CREATE TYPE xxtype AS (avar int); + +CREATE VARIABLE xxtab AS xxtype; + +INSERT INTO xxtab VALUES(10); + +-- it is ambiguous, but columns are preferred +SELECT xxtab.avar FROM xxtab; + +-- should be ok +SELECT avar FROM xxtab; + +CREATE VARIABLE public.avar AS int; + +-- should be ok, see the table +SELECT avar FROM xxtab; + +-- should be ok +SELECT public.avar FROM xxtab; + +DROP VARIABLE xxtab; + +SELECT xxtab.avar FROM xxtab; + +DROP VARIABLE public.avar; + +DROP TYPE xxtype; + +DROP TABLE xxtab; + +-- The variable can be shadowed by table or by alias +CREATE TYPE public.svar_type AS (a int, b int, c int); +CREATE VARIABLE public.svar AS public.svar_type; + +CREATE TABLE public.svar(a int, b int); + +INSERT INTO public.svar VALUES(10, 20); + +LET public.svar = (100, 200, 300); + +-- should be ok +-- show table +SELECT * FROM public.svar; +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +SELECT svar.a FROM public.svar; +SELECT svar.* FROM public.svar; + +-- show variable +SELECT public.svar; +SELECT public.svar.c; +SELECT (public.svar).*; + +-- the variable is shadowed, raise error +SELECT public.svar.c FROM public.svar; + +-- can be fixed by alias +SELECT public.svar.c FROM public.svar x; + +DROP VARIABLE public.svar; +DROP TABLE public.svar; +DROP TYPE public.svar_type; + +-- The sequence ident.* cannot be used by reference to session variable, +-- and in this case, there is not collision +CREATE SCHEMA vartest; +SET SEARCH_PATH TO vartest; +CREATE TABLE vartest_foo(a int, b int); +INSERT INTO vartest_foo VALUES(10,20), (30,40); + +CREATE VARIABLE var1 AS vartest_foo; +LET var1 = (100,200); + +-- should to fail +SELECT var1.* FROM vartest_foo; + +-- should be ok +SELECT var1.* FROM vartest_foo var1; + +-- should to fail +SELECT count(var1.*) FROM vartest_foo; + +-- should be ok +SELECT count(var1.*) FROM vartest_foo var1; + +SET SEARCH_PATH TO DEFAULT; +DROP SCHEMA vartest CASCADE; + +CREATE TYPE ab AS (a integer, b integer); + +CREATE VARIABLE v_ab AS ab; + +CREATE TABLE v_ab (a integer, b integer); +INSERT INTO v_ab VALUES(10,20); + +-- we should see table +SELECT v_ab.a FROM v_ab; + +CREATE SCHEMA v_ab; + +CREATE VARIABLE v_ab.a AS integer; + +-- we should see table +SELECT v_ab.a FROM v_ab; + +DROP VARIABLE v_ab; +DROP TABLE v_ab; +DROP TYPE ab; +DROP VARIABLE v_ab.a; +DROP SCHEMA v_ab; + +CREATE TYPE t_am_type AS (b int); +CREATE SCHEMA xxx_am; + +SET search_path TO public; + +CREATE VARIABLE xxx_am AS t_am_type; +LET xxx_am = ROW(10); + +-- should be ok +SELECT xxx_am; + +CREATE VARIABLE xxx_am.b AS int; +LET :"DBNAME".xxx_am.b = 20; + +-- should be still ok +SELECT xxx_am; + +-- should fail, the reference should be ambiguous +SELECT xxx_am.b; + +-- enhanced references should be ok +SELECT public.xxx_am.b; +SELECT :"DBNAME".xxx_am.b; + +CREATE TABLE xxx_am(b int); +INSERT INTO xxx_am VALUES(10); + +-- we should see table +SELECT xxx_am.b FROM xxx_am; +SELECT x.b FROM xxx_am x; + +DROP TABLE xxx_am; +DROP VARIABLE public.xxx_am; +DROP VARIABLE xxx_am.b; +DROP SCHEMA xxx_am; + +CREATE SCHEMA :"DBNAME"; + +CREATE VARIABLE :"DBNAME".:"DBNAME".:"DBNAME" AS t_am_type; +CREATE VARIABLE :"DBNAME".:"DBNAME".b AS int; + +SET search_path TO :"DBNAME"; + +-- should be ambiguous +SELECT :"DBNAME".b; + +-- should be ambiguous too +SELECT :"DBNAME".:"DBNAME".b; + +CREATE TABLE :"DBNAME"(b int); + +-- should be ok +SELECT :"DBNAME".b FROM :"DBNAME"; + +DROP TABLE :"DBNAME"; + +DROP VARIABLE :"DBNAME".:"DBNAME".b; +DROP VARIABLE :"DBNAME".:"DBNAME".:"DBNAME"; +DROP SCHEMA :"DBNAME"; + +RESET search_path; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 53b414bd982..014d4219e8b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1528,6 +1528,7 @@ LargeObjectDesc Latch LauncherLastStartTimesEntry LerpFunc +LetStmt LexDescr LexemeEntry LexemeHashKey @@ -2677,6 +2678,7 @@ SerializedTransactionState Session SessionBackupState SessionEndType +SessionVariableValue SetConstraintState SetConstraintStateData SetConstraintTriggerData @@ -2873,6 +2875,9 @@ SupportRequestRows SupportRequestSelectivity SupportRequestSimplify SupportRequestWFuncMonotonic +SVariable +SVariableData +SVariableState Syn SyncOps SyncRepConfigData -- 2.49.0