diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 26984b6cba..e3473a119b 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1840,7 +1840,8 @@ SCRAM-SHA-256$<iteration count>:&l m = materialized view, c = composite type, f = foreign table, - p = partitioned table + p = partitioned table, + V = schema variable diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 53a40ddeec..92848dead2 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -15883,6 +15883,83 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n); + + Schema Variable Functions + + + Schema Variable Functions + functions + + + + get_schema_variable + + + + set_schema_variable + + + + These functions allow reading and writing schema variables values. + If the schema variable referenced does not exist (created using ) + these functions will (do something...). + + + + Functions for access to schema variables + + + + Function + Argument Type + Return Type + Description + + + + + get_schema_variable(variable, expected type) + regclass, anyelement + anyelement + + Returns value of schema variable converted to expected type. + + + + + set_schema_variable(variable, value) + regclass, anyelement + void + + Sets the value of schema variable to value, after converting the input to the correct type. + + + + + +
+ + An example: + + +CREATE TEMP VARIABLE foo AS numeric; +SELECT set_schema_variable('foo', 345.445); + set_schema_variable +--------------------- + +(1 row) + +SELECT get_schema_variable('foo', null::numeric); + + get_schema_variable +--------------------- + 345.445 +(1 row) + + + +
+ System Information Functions diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index c81c87ef41..a73ad01d41 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -99,6 +99,7 @@ Complete list of usable sgml source files in this directory. + @@ -147,6 +148,7 @@ Complete list of usable sgml source files in this directory. + @@ -155,6 +157,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/create_variable.sgml b/doc/src/sgml/ref/create_variable.sgml new file mode 100644 index 0000000000..0205e4365a --- /dev/null +++ b/doc/src/sgml/ref/create_variable.sgml @@ -0,0 +1,149 @@ + + + + + CREATE VARIABLE + + + + CREATE VARIABLE + 7 + SQL - Language Statements + + + + CREATE VARIABLE + define a new permissioned typed schema variable + + + + +CREATE VARIABLE [ IF NOT EXISTS ] name [ AS ] data_type ] + + + + Description + + + CREATE VARIABLE creates a new schema variable. + These variables are scalar typed, non-transactional, and, like relations, + exist within a schema with access controlled via + GRANT and REVOKE. + + + + The value of a schema variable is session-local. Retrieving + a variable's value will return NULL unless its value has been set + to something else in the current session. + + + + Retrieval is done via the get_schema_variabledunxrion or the SQL + command SELECT. Setting of values is done via the + set_schema_variable function or the SQL command + LET. + Notably, while schema variables are in many ways a kind of table you cannot use + UPDATE on them. + + + + For purposes of name uniqueness relation-like objects (e.g., tables, indexes) + within the same schema are considered. i.e., you cannot give a table and a + schema variable the same name. This is a consequence of them being treated + like relations for purposes of SELECT. + + + + + Parameters + + + + IF NOT EXISTS + + + Do not throw an error if the name already exists. A notice is issued in this case. + Note that type of the variable is not considered, nor could it be since the namespace + searched contains non-variable objects. + + + + + + name + + + The name (optionally schema-qualified) of the variable to be created. + + + + + + data_type + + + The name (optionally schema-qualified) of the data type of the variable to be created. + + + + + + + + Notes + + + Use DROP VARIABLE to remove a variable. + + + + + Examples + + + Create an integer variable var1: + +CREATE VARIABLE var1 AS integer; + + + + + Set this variable's value; then retrieve it converted to numeric. + +CREATE VARIABLE +postgres=# select set_schema_variable('var1', 10); + set_schema_variable +--------------------- + +(1 row) + +postgres=# select get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 10 +(1 row) + + + + + + Compatibility + + + CREATE VARIABLE is a PostgreSQL feature. + + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml index 6b909b7232..395453bba0 100644 --- a/doc/src/sgml/ref/discard.sgml +++ b/doc/src/sgml/ref/discard.sgml @@ -21,7 +21,7 @@ PostgreSQL documentation -DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } +DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP | VARIABLES} @@ -75,6 +75,16 @@ DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP } + + VARIABLES + + + Sets the value of all schema variables to NULL. + + + + + ALL diff --git a/doc/src/sgml/ref/drop_variable.sgml b/doc/src/sgml/ref/drop_variable.sgml new file mode 100644 index 0000000000..06130fd510 --- /dev/null +++ b/doc/src/sgml/ref/drop_variable.sgml @@ -0,0 +1,92 @@ + + + + + DROP VARIABLE + + + + DROP VARIABLE + 7 + SQL - Language Statements + + + + DROP VARIABLE + remove a schema variable + + + + +DROP VARIABLE [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP VARIABLE removes a schema variable. + A variable can only be dropped by its owner or a superuser. + + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the variable does not exist. A notice is issued + in this case. + + + + + + name + + + The name (optionally schema-qualified) of a schema variable. + + + + + + + + Examples + + + To remove the schema variable var1: + + +DROP VARIABLE var1; + + + + + Compatibility + + + DROP VARIABLE is proprietary PostgreSQL command. + + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index ff64c7a3ba..006364ebe5 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -79,6 +79,12 @@ GRANT { USAGE | ALL [ PRIVILEGES ] } ON TYPE type_name [, ...] TO role_specification [, ...] [ WITH GRANT OPTION ] +GRANT { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE variable_name [, ...] + | ALL VARIABLES IN SCHEMA schema_name [, ...] } + TO role_specification [, ...] [ WITH GRANT OPTION ] + where role_specification can be: [ GROUP ] role_name @@ -167,6 +173,7 @@ GRANT role_name [, ...] TO PUBLIC are as follows: @@ -204,6 +211,8 @@ GRANT role_name [, ...] TO currval function. For large objects, this privilege allows the object to be read. + For schema variables, this privilege allows the get_schema_variable + to read the variable's value. @@ -239,6 +248,9 @@ GRANT role_name [, ...] TO setval functions. For large objects, this privilege allows writing or truncating the object. + For schema variables, this privilege allows LET + and set_schema_variable to modify the schema variable's + value. diff --git a/doc/src/sgml/ref/let.sgml b/doc/src/sgml/ref/let.sgml new file mode 100644 index 0000000000..e8bf3f6dd4 --- /dev/null +++ b/doc/src/sgml/ref/let.sgml @@ -0,0 +1,90 @@ + + + + + LET + + + + LET + 7 + SQL - Language Statements + + + + LET + change a schema variable's value + + + + +LET schema_variable = sql_expression + + + + + Description + + + The LET command updates the specified schema variable' value. + + + + + + Parameters + + + + schema_variable + + + The name of schema variable. + + + + + + sql expression + + + An SQL expression, the result is cast to the schema variable's type. + + + + + + + Example: + +CREATE VARIABLE myvar AS integer; +LET myvar = 10; +LET myvar = (SELECT sum(val) FROM tab); + + + + + + Compatibility + + + + LET extends syntax defined in the SQL + standard. The standard knows SET command, + that is used for different purpouse in PostgreSQL. + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 7018202f14..73778f01f9 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -108,6 +108,14 @@ REVOKE [ GRANT OPTION FOR ] REVOKE [ ADMIN OPTION FOR ] role_name [, ...] FROM role_name [, ...] [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] + { { SELECT | UPDATE } + [, ...] | ALL [ PRIVILEGES ] } + ON { VARIABLE variable_name [, ...] + | ALL VARIABLES IN SCHEMA schema_name [, ...] } + FROM { [ GROUP ] role_name | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index 73ef212c08..c3c5d9d106 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -127,6 +127,7 @@ &createType; &createUser; &createUserMapping; + &createVariable; &createView; &deallocate; &declare; @@ -175,6 +176,7 @@ &dropType; &dropUser; &dropUserMapping; + &dropVariable; &dropView; &end; &execute; @@ -183,6 +185,7 @@ &grant; &importForeignSchema; &insert; + &let; &listen; &load; &lock; diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 578e4c6592..69ea16e6a7 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -284,6 +284,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_TYPE: whole_mask = ACL_ALL_RIGHTS_TYPE; break; + case OBJECT_VARIABLE: + whole_mask = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -507,6 +510,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_FOREIGN_SERVER; errormsg = gettext_noop("invalid privilege type %s for foreign server"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for schema variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -577,6 +584,7 @@ ExecGrantStmt_oids(InternalGrant *istmt) { case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_VARIABLE: ExecGrant_Relation(istmt); break; case OBJECT_DATABASE: @@ -646,6 +654,7 @@ objectNamesToOids(ObjectType objtype, List *objnames) { case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_VARIABLE: foreach(cell, objnames) { RangeVar *relvar = (RangeVar *) lfirst(cell); @@ -1018,6 +1027,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_SCHEMA; errormsg = gettext_noop("invalid privilege type %s for schema"); break; + case OBJECT_VARIABLE: + all_privileges = ACL_ALL_RIGHTS_VARIABLE; + errormsg = gettext_noop("invalid privilege type %s for schema variable"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1215,6 +1228,12 @@ SetDefaultACL(InternalDefaultACL *iacls) this_privileges = ACL_ALL_RIGHTS_SCHEMA; break; + case OBJECT_VARIABLE: + objtype = DEFACLOBJ_VARIABLE; + if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) + this_privileges = ACL_ALL_RIGHTS_VARIABLE; + break; + default: elog(ERROR, "unrecognized objtype: %d", (int) iacls->objtype); @@ -1441,6 +1460,9 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) case DEFACLOBJ_NAMESPACE: iacls.objtype = OBJECT_SCHEMA; break; + case DEFACLOBJ_VARIABLE: + iacls.objtype = OBJECT_VARIABLE; + break; default: /* Shouldn't get here */ elog(ERROR, "unexpected default ACL type: %d", @@ -1612,8 +1634,9 @@ expand_all_col_privileges(Oid table_oid, Form_pg_class classForm, if (curr_att == ObjectIdAttributeNumber && !classForm->relhasoids) continue; - /* Views don't have any system columns at all */ - if (classForm->relkind == RELKIND_VIEW && curr_att < 0) + /* Views and variables don't have any system columns at all */ + if ((classForm->relkind == RELKIND_VIEW || classForm->relkind == RELKIND_VARIABLE) && + curr_att < 0) continue; attTuple = SearchSysCache2(ATTNUM, @@ -3456,6 +3479,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("permission denied for type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("permission denied for schema variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("permission denied for view %s"); break; @@ -3566,6 +3592,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_TYPE: msg = gettext_noop("must be owner of type %s"); break; + case OBJECT_VARIABLE: + msg = gettext_noop("must be owner of schema variable %s"); + break; case OBJECT_VIEW: msg = gettext_noop("must be owner of view %s"); break; @@ -3682,6 +3711,7 @@ pg_aclmask(ObjectType objtype, Oid table_oid, AttrNumber attnum, Oid roleid, pg_attribute_aclmask(table_oid, attnum, roleid, mask, how); case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_VARIABLE: return pg_class_aclmask(table_oid, roleid, mask, how); case OBJECT_DATABASE: return pg_database_aclmask(table_oid, roleid, mask, how); diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 39813de991..f1acc0aa70 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -294,6 +294,7 @@ heap_create(const char *relname, switch (relkind) { case RELKIND_VIEW: + case RELKIND_VARIABLE: case RELKIND_COMPOSITE_TYPE: case RELKIND_FOREIGN_TABLE: case RELKIND_PARTITIONED_TABLE: @@ -431,7 +432,8 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind, * Skip this for a view or type relation, since those don't have system * attributes. */ - if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE) + if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE && + relkind != RELKIND_VARIABLE) { for (i = 0; i < natts; i++) { @@ -720,7 +722,8 @@ AddNewAttributeTuples(Oid new_rel_oid, * all for a view or type relation. We don't bother with making datatype * dependencies here, since presumably all these types are pinned. */ - if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE) + if (relkind != RELKIND_VIEW && relkind != RELKIND_COMPOSITE_TYPE && + relkind != RELKIND_VARIABLE) { for (i = 0; i < (int) lengthof(SysAtt); i++) { @@ -1073,7 +1076,9 @@ heap_create_with_catalog(const char *relname, if (existing_relid != InvalidOid) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", relname))); + errmsg("%s \"%s\" already exists", + relkind == RELKIND_VARIABLE ? "variable" : "relation", + relname))); /* * Since we are going to create a rowtype as well, also check for @@ -1114,7 +1119,7 @@ heap_create_with_catalog(const char *relname, (relkind == RELKIND_RELATION || relkind == RELKIND_SEQUENCE || relkind == RELKIND_VIEW || relkind == RELKIND_MATVIEW || relkind == RELKIND_COMPOSITE_TYPE || relkind == RELKIND_FOREIGN_TABLE || - relkind == RELKIND_PARTITIONED_TABLE)) + relkind == RELKIND_PARTITIONED_TABLE || relkind == RELKIND_VARIABLE)) { if (!OidIsValid(binary_upgrade_next_heap_pg_class_oid)) ereport(ERROR, @@ -1156,6 +1161,10 @@ heap_create_with_catalog(const char *relname, relacl = get_user_default_acl(OBJECT_SEQUENCE, ownerid, relnamespace); break; + case RELKIND_VARIABLE: + relacl = get_user_default_acl(OBJECT_VARIABLE, ownerid, + relnamespace); + break; default: relacl = NULL; break; @@ -1189,14 +1198,16 @@ heap_create_with_catalog(const char *relname, * Decide whether to create an array type over the relation's rowtype. We * do not create any array types for system catalogs (ie, those made * during initdb). We do not create them where the use of a relation as - * such is an implementation detail: toast tables, sequences and indexes. + * such is an implementation detail: toast tables, sequences, indexes and + * variables. */ if (IsUnderPostmaster && (relkind == RELKIND_RELATION || relkind == RELKIND_VIEW || relkind == RELKIND_MATVIEW || relkind == RELKIND_FOREIGN_TABLE || relkind == RELKIND_COMPOSITE_TYPE || - relkind == RELKIND_PARTITIONED_TABLE)) + relkind == RELKIND_PARTITIONED_TABLE || + relkind == RELKIND_VARIABLE)) new_array_oid = AssignTypeArrayOid(); /* diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index ef3ea64bd0..f74f27c8e3 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -519,6 +519,9 @@ static const struct object_type_map { "sequence", OBJECT_SEQUENCE }, + { + "variable", OBJECT_VARIABLE + }, { "toast table", -1 }, /* unmapped */ @@ -823,6 +826,7 @@ get_object_address(ObjectType objtype, Node *object, case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: + case OBJECT_VARIABLE: address = get_relation_by_qualified_name(objtype, castNode(List, object), &relation, lockmode, @@ -1259,6 +1263,14 @@ get_relation_by_qualified_name(ObjectType objtype, List *object, errmsg("\"%s\" is not a foreign table", RelationGetRelationName(relation)))); break; + case OBJECT_VARIABLE: + if (relation->rd_rel->relkind != RELKIND_VARIABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a schema variable", + RelationGetRelationName(relation)))); + break; + default: elog(ERROR, "unrecognized objtype: %d", (int) objtype); break; @@ -1848,6 +1860,8 @@ get_object_address_defacl(List *object, bool missing_ok) case DEFACLOBJ_NAMESPACE: objtype_str = "schemas"; break; + case DEFACLOBJ_VARIABLE: + objtype_str = "variables"; default: ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -2110,6 +2124,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) { case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_VARIABLE: case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_INDEX: @@ -2234,6 +2249,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_INDEX: case OBJECT_SEQUENCE: case OBJECT_TABLE: + case OBJECT_VARIABLE: case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: @@ -3301,6 +3317,11 @@ getObjectDescription(const ObjectAddress *object) _("default privileges on new schemas belonging to role %s"), GetUserNameFromId(defacl->defaclrole, false)); break; + case DEFACLOBJ_VARIABLE: + appendStringInfo(&buffer, + _("default privileges on new schema variables belonging to role %s"), + GetUserNameFromId(defacl->defaclrole, false)); + break; default: /* shouldn't get here */ appendStringInfo(&buffer, @@ -3504,6 +3525,10 @@ getRelationDescription(StringInfo buffer, Oid relid) appendStringInfo(buffer, _("sequence %s"), relname); break; + case RELKIND_VARIABLE: + appendStringInfo(buffer, _("variable %s"), + relname); + break; case RELKIND_TOASTVALUE: appendStringInfo(buffer, _("toast table %s"), relname); @@ -4832,6 +4857,10 @@ getObjectIdentityParts(const ObjectAddress *object, appendStringInfoString(&buffer, " on schemas"); break; + case DEFACLOBJ_VARIABLE: + appendStringInfoString(&buffer, + " on schema variables"); + break; } if (objname) @@ -5124,6 +5153,8 @@ get_relkind_objtype(char relkind) return OBJECT_INDEX; case RELKIND_SEQUENCE: return OBJECT_SEQUENCE; + case RELKIND_VARIABLE: + return OBJECT_VARIABLE; case RELKIND_VIEW: return OBJECT_VIEW; case RELKIND_MATVIEW: diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 4a6c99e090..5747272c9a 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -18,7 +18,7 @@ OBJS = amcmds.o aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o \ event_trigger.o explain.o extension.o foreigncmds.o functioncmds.o \ indexcmds.o lockcmds.o matview.o operatorcmds.o opclasscmds.o \ policy.o portalcmds.o prepare.o proclang.o publicationcmds.o \ - schemacmds.o seclabel.o sequence.o statscmds.o subscriptioncmds.o \ + schemacmds.o schemavar.o seclabel.o sequence.o statscmds.o subscriptioncmds.o \ tablecmds.o tablespace.o trigger.o tsearchcmds.o typecmds.o user.o \ vacuum.o vacuumlazy.o variable.o view.o diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index eff325cc7d..8a4071c927 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -348,6 +348,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_MATVIEW: case OBJECT_INDEX: case OBJECT_FOREIGN_TABLE: + case OBJECT_VARIABLE: return RenameRelation(stmt); case OBJECT_COLUMN: @@ -478,6 +479,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TABLE: case OBJECT_VIEW: case OBJECT_MATVIEW: + case OBJECT_VARIABLE: address = AlterTableNamespace(stmt, oldSchemaAddr ? &oldNspOid : NULL); break; diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c index 2f2e69b4a8..f4a3cae6b4 100644 --- a/src/backend/commands/comment.c +++ b/src/backend/commands/comment.c @@ -83,7 +83,7 @@ CommentObject(CommentStmt *stmt) /* * Allow comments only on columns of tables, views, materialized - * views, composite types, and foreign tables (which are the only + * views, composite types, variables and foreign tables (which are the only * relkinds for which pg_dump will dump per-column comments). In * particular we wish to disallow comments on index columns, * because the naming of an index's columns may change across PG @@ -95,10 +95,11 @@ CommentObject(CommentStmt *stmt) relation->rd_rel->relkind != RELKIND_MATVIEW && relation->rd_rel->relkind != RELKIND_COMPOSITE_TYPE && relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE && - relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && + relation->rd_rel->relkind != RELKIND_VARIABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not a table, view, materialized view, composite type, or foreign table", + errmsg("\"%s\" is not a table, view, materialized view, composite type, schema variable or foreign table", RelationGetRelationName(relation)))); break; default: diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 770c75fe2c..5cd8a8e42c 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -1503,6 +1503,9 @@ BeginCopy(ParseState *pstate, Assert(query->utilityStmt == NULL); + /* Don't expect LET stmt here, is not possible to do write it */ + Assert(query->commandType != CMD_LET); + /* * Similarly the grammar doesn't enforce the presence of a RETURNING * clause, but this is required here. diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 01a999c2ac..7aa7d835f8 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -18,6 +18,7 @@ #include "commands/async.h" #include "commands/discard.h" #include "commands/prepare.h" +#include "commands/schemavar.h" #include "commands/sequence.h" #include "utils/guc.h" #include "utils/portal.h" @@ -25,7 +26,7 @@ static void DiscardAll(bool isTopLevel); /* - * DISCARD { ALL | SEQUENCES | TEMP | PLANS } + * DISCARD { ALL | SEQUENCES | TEMP | PLANS | VARIABLES} */ void DiscardCommand(DiscardStmt *stmt, bool isTopLevel) @@ -48,6 +49,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel) ResetTempTableNamespace(); break; + case DISCARD_VARIABLES: + ResetSchemaVariablesCache(); + break; + default: elog(ERROR, "unrecognized DISCARD target: %d", stmt->target); } @@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel) ResetPlanCache(); ResetTempTableNamespace(); ResetSequenceCaches(); + ResetSchemaVariablesCache(); } diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index eecc85d14e..62799ed284 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -126,6 +126,7 @@ static event_trigger_support_data event_trigger_support[] = { {"TEXT SEARCH TEMPLATE", true}, {"TYPE", true}, {"USER MAPPING", true}, + {"VARIABLE", true}, {"VIEW", true}, {NULL, false} }; @@ -1146,6 +1147,7 @@ EventTriggerSupportsObjectType(ObjectType obtype) case OBJECT_TSTEMPLATE: case OBJECT_TYPE: case OBJECT_USER_MAPPING: + case OBJECT_VARIABLE: case OBJECT_VIEW: return true; @@ -2244,6 +2246,8 @@ stringify_grant_objtype(ObjectType objtype) return "TABLESPACE"; case OBJECT_TYPE: return "TYPE"; + case OBJECT_VARIABLE: + return "VARIABLE"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: @@ -2326,6 +2330,8 @@ stringify_adefprivs_objtype(ObjectType objtype) return "TABLESPACES"; case OBJECT_TYPE: return "TYPES"; + case OBJECT_VARIABLE: + return "VARIABLES"; /* these currently aren't used */ case OBJECT_ACCESS_METHOD: case OBJECT_AGGREGATE: diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index e1a62a1bce..fc4469cb6c 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -946,6 +946,9 @@ ExplainNode(PlanState *planstate, List *ancestors, case CMD_DELETE: pname = operation = "Delete"; break; + case CMD_LET: + pname = operation = "Let"; + break; default: pname = "???"; break; diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index b945b1556a..a69471e926 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -151,6 +151,7 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString, case CMD_INSERT: case CMD_UPDATE: case CMD_DELETE: + case CMD_LET: /* OK */ break; default: diff --git a/src/backend/commands/schemavar.c b/src/backend/commands/schemavar.c new file mode 100644 index 0000000000..7ddf91708f --- /dev/null +++ b/src/backend/commands/schemavar.c @@ -0,0 +1,1279 @@ +/*------------------------------------------------------------------------- + * + * schemavar.c + * PostgreSQL session variable support code. + * + * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/schemavar.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" +#include "miscadmin.h" +#include "funcapi.h" + +#include "access/htup_details.h" +#include "access/xact.h" +#include "catalog/objectaddress.h" +#include "catalog/namespace.h" +#include "catalog/pg_cast.h" +#include "catalog/pg_class.h" +#include "catalog/pg_type.h" +#include "commands/defrem.h" +#include "commands/tablecmds.h" +#include "commands/schemavar.h" +#include "nodes/makefuncs.h" +#include "optimizer/planner.h" +#include "parser/parse_coerce.h" +#include "parser/parse_type.h" +#include "rewrite/rewriteHandler.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/hsearch.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/typcache.h" + +/* + * This schema variable cache mixes the cache and storages behave. That is not + * correct and it is problematic, when variable is removed. The own storage + * based on storage manager can be implemented, RelFileNode can be defined and + * mechanism based on PendingRelDelete struct can be used. This is a argument + * for implementation schema variables based on pg_class. + * Alternative solution can be detection of schema changes and recheck at and + * of transaction. + */ +typedef struct SchemaVarData +{ + Oid varid; /* pg_class OID of this sequence (hash key) */ + Oid typid; /* OID of the data type */ + int32 typmod; + int16 typlen; + bool typbyval; + bool isnull; + bool freeval; + Datum value; + bool is_rowtype; /* true when variable is composite */ +} SchemaVarData; + +typedef SchemaVarData *SchemaVar; + +static HTAB *schemavarhashtab = NULL; /* hash table for session variables */ +static MemoryContext SchemaVarMemoryContext = NULL; + +static Datum datumCast(Datum value, + Oid target_typid, int target_typmod, + Oid source_typid, int source_typmod); + +static bool first_time = true; +static bool cache_is_valid = true; + +static void InvalidateSchemaVarCacheCallback(Datum arg, int cacheid, uint32 hashvalue); +static void create_schemavar_hashtable(void); + +/* just mark cache to recheck */ +static void +InvalidateSchemaVarCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +{ + /* + * because this cache holds values of schema variables, then + * the content cannot be removed in this momemt. We should to + * wait on transaction end. + */ + cache_is_valid = false; +} + +/* + * Cast datum + */ +static Datum +datumCast(Datum value, + Oid target_typid, int target_typmod, + Oid source_typid, int source_typmod) +{ + CoercionPathType cpathtype; + Oid cfuncid; + Datum result = (Datum) 0; + bool is_binary_cast = false; + + if (target_typid != source_typid) + { + cpathtype = find_coercion_pathway(target_typid, source_typid, + COERCION_EXPLICIT, + &cfuncid); + + if (cpathtype == COERCION_PATH_NONE) + elog(ERROR, "could not find cast from %s to %s", + format_type_be(source_typid), + format_type_be(target_typid)); + + if (cpathtype == COERCION_PATH_RELABELTYPE) + { + result = value; + is_binary_cast = true; + } + else if (cpathtype == COERCION_PATH_COERCEVIAIO) + { + Oid outfunc; + Oid infunc; + Oid ioparam; + bool isVarlena; + char *str; + + getTypeOutputInfo(source_typid, &outfunc, &isVarlena); + str = OidOutputFunctionCall(outfunc, value); + + getTypeInputInfo(target_typid, &infunc, &ioparam); + result = OidInputFunctionCall(infunc, str, ioparam, -1); + } + else if (cpathtype == COERCION_PATH_FUNC) + { + result = OidFunctionCall3(cfuncid, + value, + Int32GetDatum(target_typmod), + BoolGetDatum(false)); + } + } + else + { + result = value; + is_binary_cast = true; + } + + if (target_typmod < 1 || (target_typmod == source_typmod && is_binary_cast)) + return result; + + cpathtype = find_typmod_coercion_function(target_typid, &cfuncid); + if (cpathtype == COERCION_PATH_FUNC) + { + result = OidFunctionCall3(cfuncid, + result, + Int32GetDatum(target_typmod), + BoolGetDatum(false)); + } + + return result; +} + +static bool +sv_get_typ_typmod(Form_pg_class classform, Oid *typ, int32 *typmod) +{ + if (classform->relkind != RELKIND_VARIABLE) + return false; + + if (classform->reloftype != InvalidOid) + { + Oid typoid = classform->reloftype; + + if (get_typtype(typoid) == TYPTYPE_COMPOSITE) + { + *typ = classform->reloftype; + *typmod = -1; + } + else + { + TupleDesc tupdesc; + Relation rel; + Form_pg_attribute attrStruct; + + /* find first field */ + rel = relation_open(typeidTypeRelid(classform->reltype), AccessShareLock); + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); + + attrStruct = TupleDescAttr(tupdesc, 0); + + *typ = attrStruct->atttypid; + *typmod = attrStruct->atttypmod; + + FreeTupleDesc(tupdesc); + relation_close(rel, AccessShareLock); + } + } + else + { + *typ = classform->reltype; + *typmod = -1; + } + + return true; +} + +void +schemavar_get_typ_typmod(Oid varid, Oid *typ, int32 *typmod) +{ + HeapTuple tp; + Form_pg_class classform; + + /* now, type info for schema variable is collected */ + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup faild for variable %u", varid); + + classform = (Form_pg_class) GETSTRUCT(tp); + + if (!sv_get_typ_typmod(classform, typ, typmod)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("object %u is not a schema variable", varid))); + + ReleaseSysCache(tp); +} + +bool +is_schemavar(Oid varid, bool *is_rowtype) +{ + HeapTuple tp; + Form_pg_class classform; + bool result; + Oid typ; + int32 typmod; + + if (!OidIsValid(varid)) + return false; + + /* now, type info for schema variable is collected */ + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup faild for variable %u", varid); + + classform = (Form_pg_class) GETSTRUCT(tp); + + result = sv_get_typ_typmod(classform, &typ, &typmod); + + if (result && is_rowtype) + *is_rowtype = type_is_rowtype(typ); + + ReleaseSysCache(tp); + + return result; +} + + +static SchemaVar +PrepareSchemaVar(Oid varid) +{ + SchemaVar var; + bool found; + + if (schemavarhashtab == NULL) + create_schemavar_hashtable(); + + var = (SchemaVar) hash_search(schemavarhashtab, &varid, + HASH_ENTER, &found); + if (!found) + { + Relation rel; + TupleDesc desc; + int natts; + Datum *values; + bool *nulls; + EState *estate = NULL; + MemoryContext oldcontext = NULL; + int nnotnulls = 0; + int i; + + var->varid = varid; + + schemavar_get_typ_typmod(varid, &var->typid, &var->typmod); + get_typlenbyval(var->typid, &var->typlen, &var->typbyval); + + var->is_rowtype = type_is_rowtype(var->typid); + + /* prepare default value */ + rel = relation_open(varid, AccessShareLock); + desc = RelationGetDescr(rel); + natts = desc->natts; + + estate = CreateExecutorState(); + oldcontext = MemoryContextSwitchTo(estate->es_query_cxt); + + values = (Datum *) palloc0(natts * sizeof(Datum)); + nulls = (bool *) palloc0(natts * sizeof(bool)); + + for (i = 0; i < natts; i++) + { + Expr *defexpr; + ExprState *defexprs; + + if (TupleDescAttr(desc, i)->attisdropped) + { + nulls[i] = true; + continue; + } + + defexpr = (Expr *) build_column_default(rel, i + 1); + if (defexpr != NULL) + { + defexpr = expression_planner(defexpr); + defexprs = ExecInitExpr(defexpr, NULL); + + values[i] = ExecEvalExprSwitchContext(defexprs, GetPerTupleExprContext(estate), &nulls[i]); + if (!nulls[i]) + nnotnulls++; + } + else + nulls[i] = true; + } + + MemoryContextSwitchTo(oldcontext); + + if (nnotnulls > 0) + { + Datum value; + + if (var->is_rowtype) + { + HeapTuple tuple; + + tuple = heap_form_tuple(desc, values, nulls); + value = HeapTupleHeaderGetDatum((HeapTupleHeader) tuple->t_data); + } + else + value = values[0]; + + MemoryContextSwitchTo(SchemaVarMemoryContext); + var->value = datumCopy(value, var->typbyval, var->typlen); + var->isnull = false; + var->freeval = var->value != values[0]; + + MemoryContextSwitchTo(oldcontext); + } + else + { + var->value = (Datum) 0; + var->isnull = true; + var->freeval = false; + } + + FreeExecutorState(estate); + relation_close(rel, AccessShareLock); + } + + return var; +} + +/* + * Wait on commit or rollback and clean values that miss entry in system + * catalog. It is temporary solution (although it is working). Storage manager + * based solution will be better, but it is not necessary for this PoC. + * + * removes uncommitted or dropped schema variables, so event can be ignored. + */ +static void +recheck_schema_variables(XactEvent event, void *arg) +{ + HASH_SEQ_STATUS status; + SchemaVar var; + + if (cache_is_valid || schemavarhashtab == NULL || !IsTransactionState()) + return; + + hash_seq_init(&status, schemavarhashtab); + + while ((var = (SchemaVar) hash_seq_search(&status)) != NULL) + { + HeapTuple tp = InvalidOid; + + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(var->varid)); + if (!HeapTupleIsValid(tp)) + { + elog(DEBUG1, "variable %d is removed from cache", var->varid); + + if (var->freeval) + { + pfree(DatumGetPointer(var->value)); + var->freeval = false; + } + + if (hash_search(schemavarhashtab, + (void *) &var->varid, + HASH_REMOVE, + NULL) == NULL) + elog(ERROR, "hash table corrupted"); + } + else + ReleaseSysCache(tp); + } + cache_is_valid = true; +} + +/* + * DefineSessionVariable + * Creates a new variable related relation + */ +ObjectAddress +DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt *var) +{ + CreateStmt *stmt = makeNode(CreateStmt); + Oid typoid; + Oid varoid; + ObjectAddress address; + bool create_cast; + + /* + * If if_not_exists was given and a relation with the same name already + * exists, bail out. (Note: we needn't check this when not if_not_exists, + * because DefineRelation will complain anyway.) + */ + if (var->if_not_exists) + { + RangeVarGetAndCheckCreationNamespace(var->variable, NoLock, &varoid); + if (OidIsValid(varoid)) + { + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("variable \"%s\" already exists, skipping", + var->variable->relname))); + return InvalidObjectAddress; + } + } + + /* + * Variable can be defined by type (scalar, composite) or by + * field list. When is defined by field list, we should to create + * new type related to this variable. + */ + if (var->typeName != NULL) + { + Oid typrelid; + + Assert(var->fields == NIL); + + typoid = LookupTypeNameOid(pstate, var->typeName, false); + + if (get_typtype(typoid) == TYPTYPE_PSEUDO) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable cannot be %s", + format_type_be(varoid)))); + + typrelid = typeidTypeRelid(typoid); + + if (!OidIsValid(typrelid)) + { + /* + * when variable type is not composite type, we should to create + * aux composite type. + */ + ColumnDef *coldef = makeNode(ColumnDef); + + coldef->inhcount = 0; + coldef->is_local = true; + coldef->is_not_null = false; + coldef->is_from_type = false; + coldef->storage = 0; + coldef->raw_default = var->defexpr; + coldef->cooked_default = NULL; + coldef->collClause = NULL; + coldef->collOid = InvalidOid; + coldef->constraints = NIL; + coldef->location = -1; + + /* Use implit name same like variable name */ + coldef->colname = var->variable->relname; + coldef->typeName = var->typeName; + + stmt->tableElts = lappend(stmt->tableElts, coldef); + + create_cast = false; + } + else + { + TupleDesc tupdesc; + Relation rel; + int i; + + rel = relation_open(typrelid, AccessShareLock); + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attrStruct = TupleDescAttr(tupdesc, i); + + if (!attrStruct->attisdropped) + { + ColumnDef *coldef = makeNode(ColumnDef); + + coldef->inhcount = 0; + coldef->is_local = true; + coldef->is_not_null = false; + coldef->is_from_type = false; + coldef->storage = 0; + coldef->raw_default = NULL; + coldef->cooked_default = NULL; + coldef->collClause = NULL; + coldef->collOid = InvalidOid; + coldef->constraints = NIL; + coldef->location = -1; + + coldef->colname = pstrdup(NameStr(attrStruct->attname)); + coldef->typeName = makeTypeNameFromOid(attrStruct->atttypid, + attrStruct->atttypmod); + + stmt->tableElts = lappend(stmt->tableElts, coldef); + } + } + + FreeTupleDesc(tupdesc); + relation_close(rel, AccessShareLock); + + create_cast = true; + } + } + else + { + Assert(var->typeName == NULL); + Assert(var->fields != NIL); + + stmt->tableElts = var->fields; + + create_cast = false; + } + + stmt->relation = var->variable; + stmt->inhRelations = NIL; + stmt->constraints = NIL; + stmt->options = NIL; + stmt->oncommit = ONCOMMIT_NOOP; + stmt->tablespacename = NULL; + stmt->if_not_exists = var->if_not_exists; + + /* + * Schema variables uses reltype and reloftype fields. The reltype is + * composite always for scalar and non scalar variables. It is used when + * we need a variable's tuple descriprion. When variable type is composite, + * then is referenced without change. Scalar variable V is translated to + * composite type (v_name v_type). + * The reltype is always composite - see catalog/heap.c AddNewRelationType, + * but for schema variables based on some type is this behave unwanted. + * variable type is not compatible with base type. So in this case is better + * define reltype as domain of base type. But this change break consistency + * of reltype - so I changed nothing and implicitly generate necessary casts. + */ + stmt->ofTypename = var->typeName; + + address = DefineRelation(stmt, RELKIND_VARIABLE, InvalidOid, NULL, NULL); + Assert(address.objectId != InvalidOid); + + if (create_cast) + { + ObjectAddress c1, c2, referenced_t1, referenced_t2; + TypeName *t1 = var->typeName; + TypeName *t2 = makeTypeNameFromOid(get_rel_type_id(address.objectId), -1); + + /* + * This is ugly part - but I should to teach Postgres, so t1 and t2 types + * are castable. Unfortunatelly, CreateCast cannot to change dependency, + * so fresh dependency should be removed and recreated with DEPENDENCY_AUTO + * flag. In this case, we are sure so both types are composite, so maybe less + * ugly can be use reltype as domain type of base type instead copy. + */ + CreateCastStmt *n = makeNode(CreateCastStmt); + + n->sourcetype = t1; + n->targettype = t2; + n->func = NULL; + n->context = COERCION_IMPLICIT; + n->inout = true; + + c1 = CreateCast(n); + + n->sourcetype = t2; + n->targettype = t1; + + c2 = CreateCast(n); + + deleteDependencyRecordsFor(CastRelationId, c1.objectId, false); + deleteDependencyRecordsFor(CastRelationId, c2.objectId, false); + + referenced_t1.classId = TypeRelationId; + referenced_t1.objectId = get_rel_type_id(address.objectId); + referenced_t1.objectSubId = 0; + + referenced_t2.classId = TypeRelationId; + referenced_t2.objectId = LookupTypeNameOid(pstate, var->typeName, false); + referenced_t2.objectSubId = 0; + + recordDependencyOn(&c1, &referenced_t1, DEPENDENCY_AUTO); + recordDependencyOn(&c1, &referenced_t2, DEPENDENCY_AUTO); + recordDependencyOn(&c2, &referenced_t1, DEPENDENCY_AUTO); + recordDependencyOn(&c2, &referenced_t2, DEPENDENCY_AUTO); + } + + return address; +} + +/* + * Implementation of schemavar cache. It is question if it should be in this place, or + * it should be storage related or cache related place? But for this moment (PoC) it + * can be here. Cache is implemented as hash table with own memory context. + */ + +/* + * Create the hash table for storing schema variables + */ +static void +create_schemavar_hashtable(void) +{ + HASHCTL ctl; + + /* set callbacks */ + if (first_time) + { + + CacheRegisterSyscacheCallback(RELOID, + InvalidateSchemaVarCacheCallback, + (Datum) 0); + RegisterXactCallback(recheck_schema_variables, NULL); + + first_time = false; + } + + /* needs own long life memory context */ + if (SchemaVarMemoryContext == NULL) + { + SchemaVarMemoryContext = AllocSetContextCreate(TopMemoryContext, + "schema variables", + ALLOCSET_START_SMALL_SIZES); + } + + memset(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(Oid); + ctl.entrysize = sizeof(SchemaVarData); + ctl.hcxt = SchemaVarMemoryContext; + + schemavarhashtab = hash_create("Schema variables", 64, &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + cache_is_valid = true; +} + +/* + * Fast drop complete content of schema variables + */ +void +ResetSchemaVariablesCache(void) +{ + if (schemavarhashtab) + { + hash_destroy(schemavarhashtab); + schemavarhashtab = NULL; + } + + if (SchemaVarMemoryContext != NULL) + { + MemoryContextReset(SchemaVarMemoryContext); + } +} + +/* + * Copy datum value to schema variables cache place + */ +static void +SetValue(SchemaVar var, + Datum value, bool isNull, + Oid typid, int32 typmod) +{ + /* release previously stored value */ + if (var->freeval) + { + pfree(DatumGetPointer(var->value)); + var->freeval = false; + } + + if (!isNull) + { + MemoryContext oldcxt; + + /* + * cast the value if conversion is necessary. + * Expecting: current context is short context. + * + * QUESTION: how much should be this cast tolerant/strict? + */ + if (var->typid != typid || var->typmod != typmod) + { + value = datumCast(value, + var->typid, var->typmod, + typid, typmod); + } + + var->isnull = false; + + oldcxt = MemoryContextSwitchTo(SchemaVarMemoryContext); + + var->value = datumCopy(value, var->typbyval, var->typlen); + if (var->value != value) + var->freeval = true; + + MemoryContextSwitchTo(oldcxt); + } + else + { + var->value = (Datum) 0; + var->isnull = true; + } +} + +/* + * Returns variable name + */ +char * +get_schemavar_name(Oid varid) +{ + HeapTuple relTup; + Form_pg_class relForm; + char *nspname; + char *relname; + + relTup = SearchSysCache1(RELOID, + ObjectIdGetDatum(varid)); + if (!HeapTupleIsValid(relTup)) + elog(ERROR, "cache lookup failed for schema variable %u", varid); + relForm = (Form_pg_class) GETSTRUCT(relTup); + + /* Qualify the name if not visible in search path */ + if (RelationIsVisible(varid)) + nspname = NULL; + else + nspname = get_namespace_name(relForm->relnamespace); + + relname = quote_qualified_identifier(nspname, NameStr(relForm->relname)); + + ReleaseSysCache(relTup); + + return relname; +} + +/* + * Access functions to schema variables. + */ +void +SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod) +{ + SchemaVar var; + + var = PrepareSchemaVar(varid); + SetValue(var, value, isNull, typid, typmod); +} + +/* + * Securized versions SetSchemaVariable + */ +void +SetSchemaVariableSecure(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod) +{ + AclResult aclresult; + + /* Check permissions */ + aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid)); + + SetSchemaVariable(varid, value, isNull, typid, typmod); +} + +Datum +GetSchemaVariable(Oid varid, bool *isNull, + Oid typid, int32 typmod, + int16 typlen, bool typbyval) +{ + SchemaVar var; + Datum result = (Datum) 0; + + var = PrepareSchemaVar(varid); + + if (!var->isnull) + { + result = datumCast(var->value, typid, typmod, + var->typid, var->typmod); + *isNull = false; + + if (result == var->value) + result = datumCopy(result, typbyval, typlen); + } + else + *isNull = true; + + return result; +} + +/* + * Securized version of GetSchemaVariable + */ +Datum +GetSchemaVariableSecure(Oid varid, bool *isNull, + Oid typid, int32 typmod, + int16 typlen, bool typbyval) +{ + AclResult aclresult; + + /* Check permissions */ + aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid)); + + return GetSchemaVariable(varid, isNull, typid, typmod, typlen, typbyval); +} + +/* + * V1 function API + * + * void set_schema_variable(var regclass, value anyelement); + * anyelement get_schema_variable(var regclass, expected_type anyelement) + * + */ +Datum +set_schema_variable(PG_FUNCTION_ARGS) +{ + Oid varid; + Datum value; + bool isNull; + Oid typid; + int16 typlen; + bool typbyval; + + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for variable identity"))); + + varid = PG_GETARG_OID(0); + + if (!PG_ARGISNULL(1)) + { + value = PG_GETARG_DATUM(1); + isNull = false; + } + else + { + value = (Datum) 0; + isNull = true; + } + + typid = get_fn_expr_argtype(fcinfo->flinfo, 1); + if (typid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not determine input data type"))); + + get_typlenbyval(typid, &typlen, &typbyval); + SetSchemaVariableSecure(varid, value, isNull, typid, -1); + + PG_RETURN_VOID(); +} + +Datum +get_schema_variable(PG_FUNCTION_ARGS) +{ + Oid varid; + Oid typid; + int16 typlen; + bool typbyval; + bool isNull; + Datum result; + + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for variable identity"))); + + varid = PG_GETARG_OID(0); + + typid = get_fn_expr_argtype(fcinfo->flinfo, 1); + if (typid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not determine input data type"))); + + get_typlenbyval(typid, &typlen, &typbyval); + result = GetSchemaVariableSecure(varid, &isNull, typid, -1, typlen, typbyval); + + if (isNull) + PG_RETURN_NULL(); + else + PG_RETURN_DATUM(result); +} + +/* + * Access functions to fields of composite variables + */ +void +SetSchemaVariableField(Oid varid, const char *fieldname, + Datum value, bool isNull, + Oid typid, int32 typmod) +{ + + SchemaVar var; + TupleDesc tupDesc; + Form_pg_attribute sattr = NULL; + Datum *values; + bool *nulls; + int natts; + HeapTuple tuple; + int i; + + var = PrepareSchemaVar(varid); + + if (!var->is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("the variable \"%s\" is not of row type", + get_schemavar_name(varid)))); + + tupDesc = lookup_rowtype_tupdesc(var->typid, var->typmod); + natts = tupDesc->natts; + + /* + * We should to check correct field name, although result can be NULL, + * and this information will not be used. + */ + for (i = 0; i < natts; i++) + { + if (namestrcmp(&(TupleDescAttr(tupDesc, i)->attname), fieldname) == 0 && + !TupleDescAttr(tupDesc, i)->attisdropped) + { + sattr = TupleDescAttr(tupDesc, i); + break; + } + } + + if (sattr == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("the variable \"%s\" has not field \"%s\"", + get_schemavar_name(varid), + fieldname))); + + /* leave, when we don't need to change composite */ + if (var->isnull && isNull) + { + FreeTupleDesc(tupDesc); + return; + } + + values = (Datum *) palloc0(natts * sizeof(Datum)); + nulls = (bool *) palloc0(natts * sizeof(bool)); + + if (!var->isnull) + { + HeapTupleHeader rec = DatumGetHeapTupleHeader(var->value); + HeapTupleData tup; + + /* Build a temporary HeapTuple control structure */ + tup.t_len = HeapTupleHeaderGetDatumLength(rec); + ItemPointerSetInvalid(&(tup.t_self)); + tup.t_tableOid = InvalidOid; + tup.t_data = rec; + + heap_deform_tuple(&tup, tupDesc, values, nulls); + } + else + { + for (i = 0; i < natts; i++) + nulls[i] = true; + } + + if (!isNull) + { + values[sattr->attnum - 1] = datumCast(value, + sattr->atttypid, sattr->atttypmod, + typid, typmod); + nulls[sattr->attnum - 1] = false; + } + else + { + values[sattr->attnum - 1] = (Datum) 0; + nulls[sattr->attnum - 1] = true; + } + + tuple = heap_form_tuple(tupDesc, values, nulls); + SetValue(var, + HeapTupleHeaderGetDatum((HeapTupleHeader) tuple->t_data), + false, + var->typid, var->typmod); + + heap_freetuple(tuple); + ReleaseTupleDesc(tupDesc); +} + +Datum +GetSchemaVariableField(Oid varid, const char *fieldname, + bool *isNull, + Oid typid, int32 typmod, + int16 typlen, bool typbyval) +{ + Datum result = (Datum) 0; + Datum value = (Datum) 0; + TupleDesc tupDesc; + Form_pg_attribute sattr = NULL; + SchemaVar var; + int i; + + Assert(varid != InvalidOid); + + var = PrepareSchemaVar(varid); + + if (!var->is_rowtype) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("the variable \"%s\" is not of row type", + get_schemavar_name(varid)))); + + tupDesc = lookup_rowtype_tupdesc(var->typid, var->typmod); + + /* + * We should to check correct field name, although result can be NULL, + * and this information will not be used. + */ + for (i = 0; i < tupDesc->natts; i++) + { + if (namestrcmp(&(TupleDescAttr(tupDesc, i)->attname), fieldname) == 0 && + !TupleDescAttr(tupDesc, i)->attisdropped) + { + sattr = TupleDescAttr(tupDesc, i); + break; + } + } + + if (sattr == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("the variable \"%s\" has not field \"%s\"", + get_schemavar_name(varid), + fieldname))); + + /* + * Theoretically, we check a possibility to cast to target type everytime, + * but can be ignored, when result will be null. + */ + if (!var->isnull) + { + HeapTupleHeader tuple; + HeapTupleData tmptup; + + tuple = DatumGetHeapTupleHeader(var->value); + + /* + * heap_getattr needs a HeapTuple not a bare HeapTupleHeader. We set all + * the fields in the struct just in case user tries to inspect system + * columns. + */ + tmptup.t_len = HeapTupleHeaderGetDatumLength(tuple); + ItemPointerSetInvalid(&(tmptup.t_self)); + tmptup.t_tableOid = InvalidOid; + tmptup.t_data = tuple; + + value = heap_getattr(&tmptup, + sattr->attnum, + tupDesc, + isNull); + + if (!*isNull) + { + result = datumCast(value, typid, typmod, + sattr->atttypid, sattr->atttypmod); + + if (result == value) + result = datumCopy(result, typbyval, typlen); + } + } + + ReleaseTupleDesc(tupDesc); + + return result; +} + +void +SetSchemaVariableFieldSecure(Oid varid, const char *fieldname, + Datum value, bool isNull, + Oid typid, int32 typmod) +{ + AclResult aclresult; + + /* Check permissions */ + aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_UPDATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid)); + + SetSchemaVariableField(varid, fieldname, value, isNull, typid, typmod); +} + +Datum +GetSchemaVariableFieldSecure(Oid varid, const char *fieldname, + bool *isNull, + Oid typid, int32 typmod, + int16 typlen, bool typbyval) +{ + AclResult aclresult; + + /* + * There are not any special access to fields. Just check a + * access to variable. + */ + aclresult = pg_class_aclcheck(varid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(varid)); + + return GetSchemaVariableField(varid, fieldname, isNull, typid, typmod, typlen, typbyval); +} + +Datum +set_schema_variable_field(PG_FUNCTION_ARGS) +{ + Oid varid; + Datum value; + bool isNull; + Oid typid; + int16 typlen; + bool typbyval; + const char *fieldname; + + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for variable identity"))); + + varid = PG_GETARG_OID(0); + + if (PG_ARGISNULL(1)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for field name"))); + + fieldname = NameStr(*PG_GETARG_NAME(1)); + + if (!PG_ARGISNULL(2)) + { + value = PG_GETARG_DATUM(2); + isNull = false; + } + else + { + value = (Datum) 0; + isNull = true; + } + + typid = get_fn_expr_argtype(fcinfo->flinfo, 2); + if (typid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not determine input data type"))); + + get_typlenbyval(typid, &typlen, &typbyval); + SetSchemaVariableFieldSecure(varid, fieldname, value, isNull, typid, -1); + + PG_RETURN_VOID(); +} + +Datum +get_schema_variable_field(PG_FUNCTION_ARGS) +{ + Oid varid; + Oid typid; + int16 typlen; + bool typbyval; + bool isNull; + Datum result; + const char *fieldname; + + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for variable identity"))); + + varid = PG_GETARG_OID(0); + + if (PG_ARGISNULL(1)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for field name"))); + + fieldname = NameStr(*PG_GETARG_NAME(1)); + + typid = get_fn_expr_argtype(fcinfo->flinfo, 2); + if (typid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not determine input data type"))); + + get_typlenbyval(typid, &typlen, &typbyval); + result = GetSchemaVariableFieldSecure(varid, fieldname, &isNull, typid, -1, typlen, typbyval); + + if (isNull) + PG_RETURN_NULL(); + else + PG_RETURN_DATUM(result); + + PG_RETURN_NULL(); +} + +/* + * Results: + *= + +1. The schema variables are similar to temporary tables - but the data are not saved + in 8KB blocks, so new storage and some simple storage manager should be created. We + would not to use local buffer due allocation 8KB as minimun, creating temp file. + +2. We should to work with typmod, so pg_attribute entry should be created anytime. + +3. A risk of collisions of variable and table name will be reduced, when variables + and tables cannot to have same name. + +4. If schema variables are pg_class based, then some current syntax has sense + + We can support UPDATE and SELECT commands on variables. + + possible syntaxes: + + -- there can be a analogy with record functions + SELECT varname; + SELECT varname.field; + SELECT varname FROM varname; -- when var is scalar, then varname in target list is scalar + + UPDATE varname SET varname = 10; -- scalar version + UPDATE varname SET varname = (x,y,z) -- composite version + UPDATE varname SET varname.field1 = x, varname.field2 = y, .. + + LET varname = expr; + LET varname.field1 = expr; + LET varname = (x, y, z); -- composite version + + Possible + + LET varname(a,b) = (select (10, 20) FROM ... ) + + +5. LET cmd can be implemented as CMD (like INSERT, UPDATE, DELETE) or Utility (like + CreateTableAsSelect). Prefer first option, because there can be prepared, can be + used together with EXPLAIN, etc. + + Expected form: + LET foo = (SELECT id FROM boo WHERE some = 'hello'); + + so possibility to run EXPLAIN LET .. has good enough benefit + +6. What should be mechanism of compatibility between base type and variable type? + + CREATE VARIABLE xx AS BT + + Example: CREATE TYPE ct AS (x int, int); CREATE VARIABLE var AS ct; + var type (implicitly created) should be compatible with ct type, because we want + + LET var = (10,20)::var; LET var = (10,20)::ct; + +7. When LET command allows to specify target field, then there is possible collision + I1.I2 can be schema.variable, but should be variable.field too. It should be tested + and if both variants are possible, then exception should be raised. LET var.field = expr + is a analogy to UPDATE var SET field = expr. + + +*/ \ No newline at end of file diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 0e95037dcf..0af558c3cd 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -272,6 +272,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = { gettext_noop("index \"%s\" does not exist, skipping"), gettext_noop("\"%s\" is not an index"), gettext_noop("Use DROP INDEX to remove an index.")}, + {RELKIND_VARIABLE, + ERRCODE_UNDEFINED_TABLE, + gettext_noop("schema variable \"%s\" does not exist"), + gettext_noop("schema variable \"%s\" does not exist, skipping"), + gettext_noop("\"%s\" is not a schema variable"), + gettext_noop("Use DROP VARIABLE to remove a variable.")}, {'\0', 0, NULL, NULL, NULL, NULL} }; @@ -10268,6 +10274,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock case RELKIND_MATVIEW: case RELKIND_FOREIGN_TABLE: case RELKIND_PARTITIONED_TABLE: + case RELKIND_VARIABLE: /* ok to change owner */ break; case RELKIND_INDEX: diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index e284fd71d7..6f5aa3993e 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -33,6 +33,7 @@ #include "access/nbtree.h" #include "catalog/objectaccess.h" #include "catalog/pg_type.h" +#include "commands/schemavar.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -727,6 +728,7 @@ ExecInitExprRec(Expr *node, ExprState *state, { Param *param = (Param *) node; ParamListInfo params; + AclResult aclresult; switch (param->paramkind) { @@ -734,6 +736,23 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_PARAM_EXEC; scratch.d.param.paramid = param->paramid; scratch.d.param.paramtype = param->paramtype; + ExprEvalPushStep(state, &scratch); + break; + case PARAM_SCHEMA_VARIABLE: + /* Check permission to read schema variable */ + aclresult = pg_class_aclcheck(param->paramid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_VARIABLE, get_schemavar_name(param->paramid)); + + scratch.opcode = EEOP_PARAM_SCHEMA_VARIABLE; + scratch.d.param.paramid = param->paramid; + scratch.d.param.paramtype = param->paramtype; + scratch.d.param.paramtypmod = param->paramtypmod; + + get_typlenbyval(param->paramtype, + &scratch.d.param.paramtyplen, + &scratch.d.param.paramtypbyval); + ExprEvalPushStep(state, &scratch); break; case PARAM_EXTERN: diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 9d6e25aae5..d6d1867dfa 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -59,6 +59,7 @@ #include "access/tuptoaster.h" #include "catalog/pg_type.h" #include "commands/sequence.h" +#include "commands/schemavar.h" #include "executor/execExpr.h" #include "executor/nodeSubplan.h" #include "funcapi.h" @@ -351,6 +352,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_PARAM_EXEC, &&CASE_EEOP_PARAM_EXTERN, &&CASE_EEOP_PARAM_CALLBACK, + &&CASE_EEOP_PARAM_SCHEMA_VARIABLE, &&CASE_EEOP_CASE_TESTVAL, &&CASE_EEOP_MAKE_READONLY, &&CASE_EEOP_IOCOERCE, @@ -1000,6 +1002,23 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_PARAM_SCHEMA_VARIABLE) + { + Datum d; + bool isnull; + + d = GetSchemaVariable(op->d.param.paramid, &isnull, + op->d.param.paramtype, + -1, + op->d.param.paramtyplen, + op->d.param.paramtypbyval); + + *op->resvalue = d; + *op->resnull = isnull; + + EEO_NEXT(); + } + EEO_CASE(EEOP_PARAM_CALLBACK) { /* allow an extension module to supply a PARAM_EXTERN value */ diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 51d5bd01d3..8fbc38e4bb 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -211,6 +211,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) switch (queryDesc->operation) { case CMD_SELECT: + case CMD_LET: /* * SELECT FOR [KEY] UPDATE/SHARE and modifying CTEs need to mark @@ -775,7 +776,8 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt) /* * Fail if write permissions are requested in parallel mode for table - * (temp or non-temp), otherwise fail for any non-temp table. + * (temp or non-temp), otherwise fail for any non-temp table. Allow to + * modify content of schema variables. */ foreach(l, plannedstmt->rtable) { @@ -790,6 +792,9 @@ ExecCheckXactReadOnly(PlannedStmt *plannedstmt) if (isTempNamespace(get_rel_namespace(rte->relid))) continue; + if (rte->relkind == RELKIND_VARIABLE) + continue; + PreventCommandIfReadOnly(CreateCommandTag((Node *) plannedstmt)); } @@ -1125,6 +1130,14 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation) errmsg("cannot change TOAST relation \"%s\"", RelationGetRelationName(resultRel)))); break; + case RELKIND_VARIABLE: + /* only LET command can change a variable */ + if (operation != CMD_LET) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change schema variable \"%s\"", + RelationGetRelationName(resultRel)))); + break; case RELKIND_VIEW: /* @@ -1285,6 +1298,13 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType) errmsg("cannot lock rows in foreign table \"%s\"", RelationGetRelationName(rel)))); break; + case RELKIND_VARIABLE: + /* Must disallow this because we there are not rows */ + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot lock rows in schema variable \"%s\"", + RelationGetRelationName(rel)))); + break; default: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index f6482f8411..c44ee540b9 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -39,6 +39,7 @@ #include "access/htup_details.h" #include "access/xact.h" +#include "commands/schemavar.h" #include "commands/trigger.h" #include "executor/execPartition.h" #include "executor/executor.h" @@ -73,6 +74,7 @@ static void ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate); static TupleConversionMap *tupconv_map_for_subplan(ModifyTableState *node, int whichplan); + /* * Verify that the tuples to be produced by INSERT or UPDATE match the * target relation's rowtype @@ -1516,6 +1518,72 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, return true; } +/* ---------------------------------------------------------------- + * ExecLet + * + * For LET, we have to update target variable, + * Returns NULL, there are not RETURNING clause. + * ---------------------------------------------------------------- + */ +static TupleTableSlot * +ExecLet(ModifyTableState *mtstate, + TupleTableSlot *slot, + EState *estate, + bool canSetTag) +{ + ResultRelInfo *resultRelInfo; + Relation resultRelationDesc; + TupleDesc tupdesc; + bool isnull = true; + Datum value; + Form_pg_attribute attr = NULL; + Oid varid; + + if (slot != NULL && !slot->tts_isempty) + { + tupdesc = slot->tts_tupleDescriptor; + + Assert(tupdesc != NULL); + + /* should be checked before */ + if (tupdesc->natts != 1) + elog(ERROR, "unexpected number of attributes"); + + attr = TupleDescAttr(tupdesc, 0); + + if (!slot->tts_isnull[0]) + { + isnull = false; + value = slot->tts_values[0]; + } + } + + /* + * Now, es_result_relation_info is empty, but can be initialized + * to structure of used schema variable. + */ + resultRelInfo = estate->es_result_relation_info; + resultRelationDesc = resultRelInfo->ri_RelationDesc; + varid = resultRelationDesc->rd_id; + + if (!isnull) + { + /* expecting so variable and expression are equal */ + SetSchemaVariable(varid, value, isnull, attr->atttypid, -1); + } + else + { + SetSchemaVariable(varid, (Datum) 0, true, InvalidOid, -1); + } + + if (canSetTag) + { + Assert(estate->es_processed == 0); + (estate->es_processed)++; + } + + return NULL; +} /* * Process BEFORE EACH STATEMENT triggers @@ -1548,6 +1616,9 @@ fireBSTriggers(ModifyTableState *node) case CMD_DELETE: ExecBSDeleteTriggers(node->ps.state, resultRelInfo); break; + case CMD_LET: + /* there are no trigger */ + break; default: elog(ERROR, "unknown operation"); break; @@ -1603,6 +1674,9 @@ fireASTriggers(ModifyTableState *node) ExecASDeleteTriggers(node->ps.state, resultRelInfo, node->mt_transition_capture); break; + case CMD_LET: + /* variables has not triggers */ + break; default: elog(ERROR, "unknown operation"); break; @@ -2142,6 +2216,9 @@ ExecModifyTable(PlanState *pstate) NULL, true, node->canSetTag, false /* changingPart */ ); break; + case CMD_LET: + slot = ExecLet(node, slot, estate, node->canSetTag); + break; default: elog(ERROR, "unknown operation"); break; @@ -2552,6 +2629,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) break; case CMD_UPDATE: case CMD_DELETE: + case CMD_LET: junk_filter_needed = true; break; default: diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 08f6f67a15..e107abf6af 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2420,6 +2420,9 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount) else res = SPI_OK_UPDATE; break; + case CMD_LET: + res = SPI_OK_UTILITY; + break; default: return SPI_ERROR_OPUNKNOWN; } diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 7c045a7afe..cfa6523c9d 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3103,6 +3103,18 @@ _copySelectStmt(const SelectStmt *from) return newnode; } +static LetStmt * +_copyLetStmt(const LetStmt *from) +{ + LetStmt *newnode = makeNode(LetStmt); + + COPY_NODE_FIELD(target); + COPY_NODE_FIELD(selectStmt); + COPY_LOCATION_FIELD(location); + + return newnode; +} + static SetOperationStmt * _copySetOperationStmt(const SetOperationStmt *from) { @@ -5148,6 +5160,9 @@ copyObjectImpl(const void *from) case T_SelectStmt: retval = _copySelectStmt(from); break; + case T_LetStmt: + retval = _copyLetStmt(from); + break; case T_SetOperationStmt: retval = _copySetOperationStmt(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 6a971d0141..c701aeddf1 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1057,6 +1057,16 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b) return true; } +static bool +_equalLetStmt(const LetStmt *a, const LetStmt *b) +{ + COMPARE_NODE_FIELD(target); + COMPARE_NODE_FIELD(selectStmt); + COMPARE_LOCATION_FIELD(location); + + return true; +} + static bool _equalSetOperationStmt(const SetOperationStmt *a, const SetOperationStmt *b) { @@ -3225,6 +3235,9 @@ equal(const void *a, const void *b) case T_SelectStmt: retval = _equalSelectStmt(a, b); break; + case T_LetStmt: + retval = _equalLetStmt(a, b); + break; case T_SetOperationStmt: retval = _equalSetOperationStmt(a, b); break; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index a10014f755..a931237c1b 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3469,6 +3469,16 @@ raw_expression_tree_walker(Node *node, return true; } break; + case T_LetStmt: + { + LetStmt *stmt = (LetStmt *) node; + + if (walker(stmt->target, context)) + return true; + if (walker(stmt->selectStmt, context)) + return true; + } + break; case T_A_Expr: { A_Expr *expr = (A_Expr *) node; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 505ae0af85..aee16f4676 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -1249,12 +1249,15 @@ max_parallel_hazard_walker(Node *node, max_parallel_hazard_context *context) * PARAM_EXEC Params listed in safe_param_ids, meaning they could be * either generated within the worker or can be computed in master and * then their value can be passed to the worker. + * PARAM_SCHEMA_VARIABLE params are newer changed by workers, so they can be + * safe. */ else if (IsA(node, Param)) { Param *param = (Param *) node; - if (param->paramkind == PARAM_EXTERN) + if (param->paramkind == PARAM_EXTERN || + param->paramkind == PARAM_SCHEMA_VARIABLE) return false; if (param->paramkind != PARAM_EXEC || @@ -4793,7 +4796,7 @@ substitute_actual_parameters_mutator(Node *node, { if (node == NULL) return NULL; - if (IsA(node, Param)) + if (IsA(node, Param) && ((Param *) node)->paramkind != PARAM_SCHEMA_VARIABLE) { Param *param = (Param *) node; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 05f57591e4..4ff6c443b4 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -24,6 +24,7 @@ #include "postgres.h" +#include "access/heapam.h" #include "access/sysattr.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -42,8 +43,12 @@ #include "parser/parse_param.h" #include "parser/parse_relation.h" #include "parser/parse_target.h" +#include "parser/parse_type.h" #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/typcache.h" #include "utils/rel.h" @@ -56,6 +61,7 @@ static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt); static List *transformInsertRow(ParseState *pstate, List *exprlist, List *stmtcols, List *icolumns, List *attrnos, bool strip_indirection); +static Query *transformLetStmt(ParseState *pstate, LetStmt *stmt); static OnConflictExpr *transformOnConflictClause(ParseState *pstate, OnConflictClause *onConflictClause); static int count_rowexpr_columns(ParseState *pstate, Node *expr); @@ -267,6 +273,7 @@ transformStmt(ParseState *pstate, Node *parseTree) case T_InsertStmt: case T_UpdateStmt: case T_DeleteStmt: + case T_LetStmt: (void) test_raw_expression_coverage(parseTree, NULL); break; default: @@ -304,6 +311,10 @@ transformStmt(ParseState *pstate, Node *parseTree) } break; + case T_LetStmt: + result = transformLetStmt(pstate, (LetStmt *) parseTree); + break; + /* * Special cases */ @@ -367,6 +378,7 @@ analyze_requires_snapshot(RawStmt *parseTree) case T_DeleteStmt: case T_UpdateStmt: case T_SelectStmt: + case T_LetStmt: result = true; break; @@ -1541,6 +1553,354 @@ transformValuesClause(ParseState *pstate, SelectStmt *stmt) return qry; } +/* + * transformLetStmt - + * transform an Let Statement + */ +static Query * +transformLetStmt(ParseState *pstate, LetStmt *stmt) +{ + Query *qry = makeNode(Query); + List *exprList = NIL; + List *exprListCoer = NIL; + List *sub_rtable; + List *sub_namespace; + RangeTblEntry *rte; + RangeTblRef *rtr; + ListCell *lc; + AclMode targetPerms; + ParseState *sub_pstate; + Query *selectQuery; + int i = 0; + + RangeVar *rvar; + char *str1 = NULL; + char *str2 = NULL; + char *str3 = NULL; + char *fieldname; + int fieldname_pos; + List *indirection = NIL; + int indirection_pos = -1; + + Relation rd; + Oid vartypid = InvalidOid; + int32 vartypmod = -1; + Oid varid; + + ParseExprKind sv_expr_kind; + + sv_expr_kind = pstate->p_expr_kind; + pstate->p_expr_kind = EXPR_KIND_UPDATE_TARGET; + + /* There can't be any outer WITH to worry about */ + Assert(pstate->p_ctenamespace == NIL); + + qry->commandType = CMD_LET; + pstate->p_is_let = true; + + switch (list_length(stmt->target)) + { + case 1: + { + Node *field1 = (Node *) linitial(stmt->target); + + Assert(IsA(field1, String)); + str2 = strVal(field1); + } + break; + case 2: + { + Node *field1 = (Node *) linitial(stmt->target); + Node *field2 = (Node *) lsecond(stmt->target); + + Assert(IsA(field1, String)); + + if (IsA(field2, String)) + { + str1 = strVal(field1); + str2 = strVal(field2); + } + else + { + str2 = strVal(field1); + indirection_pos = 2; + } + } + break; + case 3: + { + Node *field1 = (Node *) linitial(stmt->target); + Node *field2 = (Node *) lsecond(stmt->target); + Node *field3 = (Node *) lthird(stmt->target); + + Assert(IsA(field1, String)); + + if (!IsA(field2, String)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("broken schema variable identifier"), + parser_errposition(pstate, stmt->location))); + + if (IsA(field2, String) && IsA(field3, String)) + { + str1 = strVal(field1); + str2 = strVal(field2); + str3 = strVal(field3); + } + else + { + str1 = strVal(field1); + str2 = strVal(field2); + indirection_pos = 3; + } + } + break; + case 4: + { + Node *field1 = (Node *) linitial(stmt->target); + Node *field2 = (Node *) lsecond(stmt->target); + Node *field3 = (Node *) lthird(stmt->target); + Node *field4 = (Node *) lfourth(stmt->target); + + Assert(IsA(field1, String)); + + if (IsA(field2, String) && IsA(field3, String) && !IsA(field4, String)) + { + str1 = strVal(field1); + str2 = strVal(field2); + str3 = strVal(field3); + indirection_pos = 4; + } + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("broken schema variable identifier"), + parser_errposition(pstate, stmt->location))); + } + break; + + + default: + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name"), + parser_errposition(pstate, stmt->location))); + } + + rvar = makeRangeVarForTargetOfSchemaVariable(pstate, str1, str2, str3, + stmt->location, &fieldname, + &fieldname_pos, false); + + /* + * If a non-nil rangetable/namespace was passed in, and we are doing + * INSERT/SELECT, arrange to pass the rangetable/namespace down to the + * SELECT. This can only happen if we are inside a CREATE RULE, and in + * that case we want the rule's OLD and NEW rtable entries to appear as + * part of the SELECT's rtable, not as outer references for it. (Kluge!) + * The SELECT's joinlist is not affected however. We must do this before + * adding the target table to the INSERT's rtable. + */ + sub_rtable = pstate->p_rtable; + pstate->p_rtable = NIL; + sub_namespace = pstate->p_namespace; + pstate->p_namespace = NIL; + + targetPerms = ACL_UPDATE; + qry->resultRelation = setTargetTable(pstate, rvar, + false, false, targetPerms); + + rd = pstate->p_target_relation; + + vartypid = rd->rd_rel->reloftype; + varid = rd->rd_id; + + if (vartypid != InvalidOid) + { + if (get_typtype(vartypid) != TYPTYPE_COMPOSITE) + { + TupleDesc tupdesc; + Relation rel; + Form_pg_attribute attrStruct; + + /* find first field */ + rel = relation_open(typeidTypeRelid(rd->rd_rel->reltype), AccessShareLock); + tupdesc = CreateTupleDescCopy(RelationGetDescr(rel)); + + attrStruct = TupleDescAttr(tupdesc, 0); + + vartypid = attrStruct->atttypid; + vartypmod = attrStruct->atttypmod; + + FreeTupleDesc(tupdesc); + relation_close(rel, AccessShareLock); + } + else + { + vartypid = rd->rd_rel->reloftype; + vartypmod = -1; + } + } + else + { + vartypid = rd->rd_rel->reltype; + vartypmod = -1; + } + + if (fieldname != NULL) + indirection = list_copy_tail(stmt->target, fieldname_pos - 1); + else if (indirection_pos >= 2) + indirection = list_copy_tail(stmt->target, indirection_pos - 1); + + /* + * We make the sub-pstate a child of the outer pstate so that it can + * see any Param definitions supplied from above. Since the outer + * pstate's rtable and namespace are presently empty, there are no + * side-effects of exposing names the sub-SELECT shouldn't be able to + * see. + */ + sub_pstate = make_parsestate(pstate); + + /* + * Process the source SELECT. + * + * It is important that this be handled just like a standalone SELECT; + * otherwise the behavior of SELECT within INSERT might be different + * from a stand-alone SELECT. (Indeed, Postgres up through 6.5 had + * bugs of just that nature...) + * + * The sole exception is that we prevent resolving unknown-type + * outputs as TEXT. This does not change the semantics since if the + * column type matters semantically, it would have been resolved to + * something else anyway. Doing this lets us resolve such outputs as + * the target column's type, which we handle below. + */ + sub_pstate->p_rtable = sub_rtable; + sub_pstate->p_joinexprs = NIL; /* sub_rtable has no joins */ + sub_pstate->p_namespace = sub_namespace; + sub_pstate->p_resolve_unknowns = false; + + selectQuery = transformStmt(sub_pstate, stmt->selectStmt); + + free_parsestate(sub_pstate); + + /* The grammar should have produced a SELECT */ + if (!IsA(selectQuery, Query) || + selectQuery->commandType != CMD_SELECT) + elog(ERROR, "unexpected non-SELECT command in LET ... SELECT"); + + /* + * Make the source be a subquery in the LET's rangetable, and add + * it to the LET's joinlist. + */ + rte = addRangeTableEntryForSubquery(pstate, + selectQuery, + makeAlias("*SELECT*", NIL), + false, + false); + rtr = makeNode(RangeTblRef); + /* assume new rte is at end */ + rtr->rtindex = list_length(pstate->p_rtable); + Assert(rte == rt_fetch(rtr->rtindex, pstate->p_rtable)); + pstate->p_joinlist = lappend(pstate->p_joinlist, rtr); + + /*---------- + * Generate an expression list for the LET that selects all the + * non-resjunk columns from the subquery. + *---------- + */ + + exprList = NIL; + foreach(lc, selectQuery->targetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (tle->resjunk) + continue; + + exprList = lappend(exprList, tle->expr); + } + + /* + * Because doesn't support pattern matching, don't allow multicolumn result + */ + if (list_length(exprList) != 1) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("expression is not scalar value"), + parser_errposition(pstate, + exprLocation((Node *) exprList)))); + + exprListCoer = NIL; + foreach(lc, exprList) + { + Node *orig_expr = (Node*) lfirst(lc); + Oid exprtypid = exprType((Node *) orig_expr); + Expr *expr = NULL; + Param *param = makeNode(Param); + + param->paramkind = PARAM_SCHEMA_VARIABLE; + param->paramid = varid; + param->paramtype = vartypid; + param->paramtypmod = vartypmod; + + expr = (Expr *) + transformAssignmentIndirection(pstate, + (Node *) param, + fieldname, + false, + vartypid, + vartypmod, + InvalidOid, + list_head(indirection), + (Node *) orig_expr, + -1); + + if (expr == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("variable \"%s\" is of type %s" + " but expression is of type %s", + RelationGetRelationName(rd), + format_type_be(vartypid), + format_type_be(exprtypid)), + errhint("You will need to rewrite or cast the expression."), + parser_errposition(pstate, exprLocation((Node *) orig_expr)))); + + exprListCoer = lappend(exprListCoer, expr); + } + + /* + * Generate query's target list using the computed list of expressions. + * Also, mark all the target columns as needing insert permissions. + */ + rte = pstate->p_target_rangetblentry; + qry->targetList = NIL; + foreach(lc, exprListCoer) + { + Expr *expr = (Expr *) lfirst(lc); + TargetEntry *tle; + + tle = makeTargetEntry(expr, + i + 1, + FigureColname((Node *)expr), + false); + qry->targetList = lappend(qry->targetList, tle); + } + + /* done building the range table and jointree */ + qry->rtable = pstate->p_rtable; + qry->jointree = makeFromExpr(pstate->p_joinlist, NULL); + + qry->hasTargetSRFs = pstate->p_hasTargetSRFs; + qry->hasSubLinks = pstate->p_hasSubLinks; + + assign_query_collations(pstate, qry); + + pstate->p_expr_kind = sv_expr_kind; + + return qry; +} + /* * transformSetOperationStmt - * transforms a set-operations tree diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 5a36367446..95d26a73b2 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -157,6 +157,8 @@ static RoleSpec *makeRoleSpec(RoleSpecType type, int location); static void check_qualified_name(List *names, core_yyscan_t yyscanner); static List *check_func_name(List *names, core_yyscan_t yyscanner); static List *check_indirection(List *indirection, core_yyscan_t yyscanner); +static List *check_indirection_let(List *indirection, core_yyscan_t yyscanner); + static List *extractArgTypes(List *parameters); static List *extractAggrArgTypes(List *aggrargs); static List *makeOrderedSetArgs(List *directargs, List *orderedargs, @@ -257,8 +259,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt - CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt - CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt + CreateSchemaStmt CreateSchemaVarStmt CreateSeqStmt CreateStmt CreateStatsStmt + CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt @@ -268,7 +270,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DropTransformStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt - ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt + LetStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt RemoveFuncStmt RemoveOperStmt RenameStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt @@ -584,6 +586,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type partbound_datum PartitionRangeDatum %type hash_partbound partbound_datum_list range_datum_list %type hash_partbound_elem +%type fieldList +%type fieldDef optSchemaVarDefExpr +%type schemavar_name /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -649,7 +654,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); KEY 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 MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE @@ -687,8 +692,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNBOUNDED UNCOMMITTED UNENCRYPTED UNION UNIQUE UNKNOWN UNLISTEN UNLOGGED UNTIL UPDATE USER USING - VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VOLATILE + VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIABLE VARIABLES + VARIADIC VARYING VERBOSE VERSION_P VIEW VIEWS VOLATILE WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE @@ -879,6 +884,7 @@ stmt : | CreatePLangStmt | CreateSchemaStmt | CreateSeqStmt + | CreateSchemaVarStmt | CreateStmt | CreateSubscriptionStmt | CreateStatsStmt @@ -920,6 +926,7 @@ stmt : | ListenStmt | RefreshMatViewStmt | LoadStmt + | LetStmt | LockStmt | NotifyStmt | PrepareStmt @@ -1380,6 +1387,7 @@ schema_stmt: CreateStmt | IndexStmt | CreateSeqStmt + | CreateSchemaVarStmt | CreateTrigStmt | GrantStmt | ViewStmt @@ -1808,7 +1816,12 @@ DiscardStmt: n->target = DISCARD_SEQUENCES; $$ = (Node *) n; } - + | DISCARD VARIABLES + { + DiscardStmt *n = makeNode(DiscardStmt); + n->target = DISCARD_VARIABLES; + $$ = (Node *) n; + } ; @@ -2004,6 +2017,24 @@ AlterTableStmt: n->nowait = $14; $$ = (Node *)n; } + | ALTER VARIABLE qualified_name alter_table_cmds + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $3; + n->cmds = $4; + n->relkind = OBJECT_VARIABLE; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS qualified_name alter_table_cmds + { + AlterTableStmt *n = makeNode(AlterTableStmt); + n->relation = $5; + n->cmds = $6; + n->relkind = OBJECT_VARIABLE; + n->missing_ok = true; + $$ = (Node *)n; + } ; alter_table_cmds: @@ -4287,6 +4318,86 @@ NumericOnly_list: NumericOnly { $$ = list_make1($1); } | NumericOnly_list ',' NumericOnly { $$ = lappend($1, $3); } ; +/***************************************************************************** + * + * QUERY : + * CREATE VARIABLE seqname [AS] type + * + *****************************************************************************/ + +CreateSchemaVarStmt: + CREATE OptTemp VARIABLE qualified_name opt_as Typename optSchemaVarDefExpr + { + CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt); + $4->relpersistence = $2; + n->variable = $4; + n->typeName = $6; + n->defexpr = $7; + n->fields = NULL; + n->if_not_exists = false; + $$ = (Node *)n; + } + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as Typename optSchemaVarDefExpr + { + CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt); + $7->relpersistence = $2; + n->variable = $7; + n->typeName = $9; + n->defexpr = $10; + n->fields = NULL; + n->if_not_exists = true; + $$ = (Node *)n; + } + | CREATE OptTemp VARIABLE qualified_name opt_as '(' fieldList ')' + { + CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt); + $4->relpersistence = $2; + n->variable = $4; + n->typeName = NULL; + n->fields = $7; + n->if_not_exists = false; + $$ = (Node *)n; + } + | CREATE OptTemp VARIABLE IF_P NOT EXISTS qualified_name opt_as '(' fieldList ')' + { + CreateSchemaVarStmt *n = makeNode(CreateSchemaVarStmt); + $7->relpersistence = $2; + n->variable = $7; + n->typeName = NULL; + n->fields = $10; + n->if_not_exists = true; + $$ = (Node *)n; + } + ; + +optSchemaVarDefExpr: DEFAULT b_expr { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + +fieldList: + fieldDef { $$ = list_make1($1); } + | fieldList ',' fieldDef { $$ = lappend($1, $3); } + ; + +fieldDef: ColId Typename optSchemaVarDefExpr + { + ColumnDef *n = makeNode(ColumnDef); + n->colname = $1; + n->typeName = $2; + n->inhcount = 0; + n->is_local = true; + n->is_not_null = false; + n->is_from_type = false; + n->is_from_parent = false; + n->storage = 0; + n->raw_default = $3; + n->cooked_default = NULL; + n->collOid = InvalidOid; + n->location = @1; + $$ = (Node *)n; + } + ; + /***************************************************************************** * * QUERIES : @@ -6335,6 +6446,7 @@ drop_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } | TEXT_P SEARCH CONFIGURATION { $$ = OBJECT_TSCONFIGURATION; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* object types taking name_list */ @@ -6604,6 +6716,7 @@ comment_type_any_name: | TEXT_P SEARCH DICTIONARY { $$ = OBJECT_TSDICTIONARY; } | TEXT_P SEARCH PARSER { $$ = OBJECT_TSPARSER; } | TEXT_P SEARCH TEMPLATE { $$ = OBJECT_TSTEMPLATE; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* object types taking name */ @@ -6742,6 +6855,7 @@ security_label_type_any_name: | TABLE { $$ = OBJECT_TABLE; } | VIEW { $$ = OBJECT_VIEW; } | MATERIALIZED VIEW { $$ = OBJECT_MATVIEW; } + | VARIABLE { $$ = OBJECT_VARIABLE; } ; /* object types taking name */ @@ -7067,6 +7181,14 @@ privilege_target: n->objs = $2; $$ = n; } + | VARIABLE qualified_name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_VARIABLE; + n->objs = $2; + $$ = n; + } | FOREIGN DATA_P WRAPPER name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -7179,6 +7301,14 @@ privilege_target: n->objs = $5; $$ = n; } + | ALL VARIABLES IN_P SCHEMA name_list + { + PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + n->targtype = ACL_TARGET_ALL_IN_SCHEMA; + n->objtype = OBJECT_VARIABLE; + n->objs = $5; + $$ = n; + } | ALL FUNCTIONS IN_P SCHEMA name_list { PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); @@ -7361,6 +7491,7 @@ defacl_privilege_target: | FUNCTIONS { $$ = OBJECT_FUNCTION; } | ROUTINES { $$ = OBJECT_FUNCTION; } | SEQUENCES { $$ = OBJECT_SEQUENCE; } + | VARIABLES { $$ = OBJECT_VARIABLE; } | TYPES_P { $$ = OBJECT_TYPE; } | SCHEMAS { $$ = OBJECT_SCHEMA; } ; @@ -8959,6 +9090,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *)n; } + | ALTER VARIABLE qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->relation = $3; + n->subname = NULL; + n->newname = $6; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER VARIABLE IF_P EXISTS qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_VARIABLE; + n->relation = $5; + n->subname = NULL; + n->newname = $8; + n->missing_ok = true; + $$ = (Node *)n; + } ; opt_column: COLUMN { $$ = COLUMN; } @@ -10686,7 +10837,8 @@ ExplainableStmt: | CreateAsStmt | CreateMatViewStmt | RefreshMatViewStmt - | ExecuteStmt /* by default all are $$=$1 */ + | ExecuteStmt + | LetStmt /* by default all are $$=$1 */ ; explain_option_list: @@ -10743,7 +10895,8 @@ PreparableStmt: SelectStmt | InsertStmt | UpdateStmt - | DeleteStmt /* by default all are $$=$1 */ + | DeleteStmt + | LetStmt /* by default all are $$=$1 */ ; /***************************************************************************** @@ -11142,6 +11295,50 @@ opt_hold: /* EMPTY */ { $$ = 0; } | WITHOUT HOLD { $$ = 0; } ; +/***************************************************************************** + * + * QUERY: + * LET STATEMENTS + * + *****************************************************************************/ +LetStmt: LET schemavar_name '=' a_expr + { + LetStmt *n = makeNode(LetStmt); + SelectStmt *select = makeNode(SelectStmt); + ResTarget *res = makeNode(ResTarget); + + res->name = NULL; + res->indirection = NIL; + res->val = (Node *) $4; + res->location = @4; + select->targetList = list_make1(res); + n->target = $2; + n->selectStmt = (Node *) select; + n->location = @2; + + $$ = (Node *) n; + } + ; + +schemavar_name: + ColId + { + $$ = list_make1(makeString($1)); + } + | ColId indirection + { + check_indirection_let($2, yyscanner); + if (list_length($2) > 3) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("improper qualified name"), + parser_errposition(@1))); + + $$ = lcons(makeString($1), $2); + $$ = list_union($$, $2); + } + ; + /***************************************************************************** * * QUERY: @@ -15101,6 +15298,7 @@ unreserved_keyword: | LARGE_P | LAST_P | LEAKPROOF + | LET | LEVEL | LISTEN | LOAD @@ -15249,6 +15447,8 @@ unreserved_keyword: | VALIDATE | VALIDATOR | VALUE_P + | VARIABLE + | VARIABLES | VARYING | VERSION_P | VIEW @@ -15713,6 +15913,25 @@ check_func_name(List *names, core_yyscan_t yyscanner) * We only allow '*' at the end of the list, but it's hard to enforce that * in the grammar, so do it here. */ +static List * +check_indirection_let(List *indirection, core_yyscan_t yyscanner) +{ + ListCell *l; + + foreach(l, indirection) + { + if (IsA(lfirst(l), A_Star)) + parser_yyerror("syntax error"); + + if (IsA(lfirst(l), A_Indices)) + { + if (lnext(l) != NULL) + parser_yyerror("improper use of []"); + } + } + return indirection; +} + static List * check_indirection(List *indirection, core_yyscan_t yyscanner) { @@ -15729,6 +15948,7 @@ check_indirection(List *indirection, core_yyscan_t yyscanner) return indirection; } + /* extractArgTypes() * Given a list of FunctionParameter nodes, extract a list of just the * argument types (TypeNames) for input parameters only. This is what diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 385e54a9b6..8f6e28da8f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -371,7 +371,7 @@ transformExprRecurse(ParseState *pstate, Node *expr) default: /* should not reach here */ - elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); + elog(ERROR, "unrecognized node type: %d %d", (int) nodeTag(expr), T_Const); result = NULL; /* keep compiler quiet */ break; } @@ -509,6 +509,7 @@ static Node * transformColumnRef(ParseState *pstate, ColumnRef *cref) { Node *node = NULL; + Node *variable = NULL; char *nspname = NULL; char *relname = NULL; char *colname = NULL; @@ -749,6 +750,133 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) break; } + /* + * Try to identify column ref as variable. Possible variants are + * + * A .. variable name + * A.B .. qualified variable name + * A.B .. variable.field + * A.B.C .. schema.variable.field + * A.B.C.D .. catalogue.schema.variable.field + */ + switch (list_length(cref->fields)) + { + case 1: + { + Node *field1 = (Node *) linitial(cref->fields); + + if (IsA(field1, String)) + { + char *varname = strVal(field1); + + /* Try to identify as an unqualified column */ + variable = toSchemaVariable(pstate, + NULL, varname, NULL, + cref->location); + } + break; + } + case 2: + { + Node *field1 = (Node *) linitial(cref->fields); + Node *field2 = (Node *) lsecond(cref->fields); + + if (IsA(field1, String) && IsA(field2, String)) + { + char *nspname = strVal(field1); + char *varname = strVal(field2); + + /* Try to identify as an unqualified column */ + variable = toSchemaVariable(pstate, + nspname, varname, NULL, + cref->location); + } + break; + } + case 3: + { + Node *field1 = (Node *) linitial(cref->fields); + Node *field2 = (Node *) lsecond(cref->fields); + Node *field3 = (Node *) lthird(cref->fields); + + if (IsA(field1, String) && IsA(field2, String) && IsA(field2, String)) + { + char *nspname = strVal(field1); + char *varname = strVal(field2); + char *fieldname = strVal(field3); + + /* Try to identify as an unqualified column */ + variable = toSchemaVariable(pstate, + nspname, varname, fieldname, + cref->location); + } + break; + } + case 4: + { + Node *field1 = (Node *) linitial(cref->fields); + Node *field2 = (Node *) lsecond(cref->fields); + Node *field3 = (Node *) lthird(cref->fields); + Node *field4 = (Node *) lfourth(cref->fields); + char *catname; + + Assert(IsA(field1, String)); + catname = strVal(field1); + + /* + * We check the catalog name and then ignore it. + */ + if (strcmp(catname, get_database_name(MyDatabaseId)) != 0) + { + crerr = CRERR_WRONG_DB; + break; + } + + if (IsA(field1, String) && IsA(field2, String) && IsA(field2, String)) + { + char *nspname = strVal(field2); + char *varname = strVal(field3); + char *fieldname = strVal(field4); + + /* Try to identify as an unqualified column */ + variable = toSchemaVariable(pstate, + nspname, varname, fieldname, + cref->location); + } + break; + } + default: + + /* + * There can be another variants, more when composite variables + * will be supported. Currently only scalars are supported, so + * there are not necessary to solve other questions. + * + * do nothing + */ + crerr = CRERR_TOO_MANY; /* too many dotted names */ + break; + } + + if (variable != NULL) + { + /* + * When we allow variables in FROM clause, then here can be check, + * if variable and node are same. Or better, when node will be + * referenced to variable, then toSchemaVariable will not be called. + * But now, we don't allow schema variable in FROM clause, so the + * conflict is not possible. + */ + if (node != NULL) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("column reference \"%s\" is ambiguous", + NameListToString(cref->fields)), + parser_errposition(pstate, cref->location))); + + node = variable; + } + /* * Now give the PostParseColumnRefHook, if any, a chance. We pass the * translation-so-far so that it can throw an error if it wishes in the diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index bf5df26009..512e364fa2 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -21,6 +21,7 @@ #include "catalog/heap.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" +#include "commands/schemavar.h" #include "funcapi.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -32,6 +33,7 @@ #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" +#include "utils/typcache.h" #include "utils/varlena.h" @@ -1150,6 +1152,7 @@ parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode) setup_parser_errposition_callback(&pcbstate, pstate, relation->location); rel = heap_openrv_extended(relation, lockmode, true); + if (rel == NULL) { if (relation->schemaname) @@ -1180,6 +1183,24 @@ parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode) relation->relname))); } } + + /* + * RELKIND_VARIABLE can be used only in LET command. + * Probably this check can be done elsewhere, but here I + * have a used relation and parse state together first time. + */ + if (rel->rd_rel->relkind == RELKIND_VARIABLE && !pstate->p_is_let) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is an schema variable", + RelationGetRelationName(rel)))); + + if (pstate->p_is_let && rel->rd_rel->relkind != RELKIND_VARIABLE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an schema variable", + RelationGetRelationName(rel)))); + cancel_parser_errposition_callback(&pcbstate); return rel; } @@ -3360,3 +3381,160 @@ isQueryUsingTempRelation_walker(Node *node, void *context) isQueryUsingTempRelation_walker, context); } + +RangeVar * +makeRangeVarForTargetOfSchemaVariable(ParseState *pstate, + char *field1, char *field2, char *field3, + int location, char **fieldname, int *fieldname_pos, bool noerror) +{ + RangeVar *rv1; + Oid varid1; + bool is_rowtype1; + + rv1 = makeRangeVar(field1, field2, location); + varid1 = RangeVarGetRelid(rv1, NoLock, true); + varid1 = is_schemavar(varid1, &is_rowtype1) ? varid1 : InvalidOid; + + if (field1 != NULL && field3 == NULL) + { + RangeVar *rv2; + bool is_rowtype2; + Oid varid2; + + rv2 = makeRangeVar(NULL, field1, location); + varid2 = RangeVarGetRelid(rv2, NoLock, true); + + /* varid2 has sense only as row type variable */ + varid2 = is_schemavar(varid2, &is_rowtype2) ? varid2 : InvalidOid; + varid2 = is_rowtype2 ? varid2 : InvalidOid; + + if (OidIsValid(varid1) && OidIsValid(varid2)) + ereport(ERROR, + (errcode(ERRCODE_AMBIGUOUS_COLUMN), + errmsg("variable reference \"%s\".\"%s\" is ambiguous", field1, field2), + parser_errposition(pstate, location))); + + if (OidIsValid(varid2)) + { + *fieldname = field2; + *fieldname_pos = 2; + return rv2; + } + } + + if (!OidIsValid(varid1)) + { + if (!noerror) + { + if (field1 != NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("schema variable \"%s.%s\" doesn't exists", field1, field2), + parser_errposition(pstate, location))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("schema variable \"%s\" doesn't exists", field2), + parser_errposition(pstate, location))); + } + + return NULL; + } + + *fieldname = field3; + *fieldname_pos = 3; + + if (field3 != NULL && !is_rowtype1) + { + if (!noerror) + { + if (field1 != NULL) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s.%s\" is not row type", field1, field2), + parser_errposition(pstate, location))); + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("variable \"%s\" is not row type", field2), + parser_errposition(pstate, location))); + } + + return NULL; + } + + return rv1; +} + +/* + * Try to replace ColumnRef by Param related to variable + * + */ +Node * +toSchemaVariable(ParseState *pstate, char *nspname, char *varname, char *fieldname, int location) +{ + char *used_fieldname; + RangeVar *rv; + Oid varid; + int fieldname_pos; + Oid vartyp; + int32 vartypmod; + Param *param; + + rv = makeRangeVarForTargetOfSchemaVariable(pstate, + nspname, varname, fieldname, location, + &used_fieldname, &fieldname_pos, true); + + if (rv == NULL) + return NULL; + + varid = RangeVarGetRelid(rv, NoLock, true); + if (!OidIsValid(varid)) + return NULL; + + param = makeNode(Param); + + schemavar_get_typ_typmod(varid, &vartyp, &vartypmod); + + param->paramkind = PARAM_SCHEMA_VARIABLE; + param->paramid = varid; + param->paramtype = vartyp; + param->paramtypmod = vartypmod; + + if (used_fieldname != NULL) + { + TupleDesc tupdesc; + int i; + + tupdesc = lookup_rowtype_tupdesc(param->paramtype, param->paramtypmod); + + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + + if (strcmp(used_fieldname, 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", used_fieldname), + parser_errposition(pstate, location))); + } + + return (Node *) param; +} diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4932e58022..8549908095 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -35,16 +35,6 @@ static void markTargetListOrigin(ParseState *pstate, TargetEntry *tle, Var *var, int levelsup); -static Node *transformAssignmentIndirection(ParseState *pstate, - Node *basenode, - const char *targetName, - bool targetIsArray, - Oid targetTypeId, - int32 targetTypMod, - Oid targetCollation, - ListCell *indirection, - Node *rhs, - int location); static Node *transformAssignmentSubscripts(ParseState *pstate, Node *basenode, const char *targetName, @@ -596,7 +586,6 @@ transformAssignedExpr(ParseState *pstate, return expr; } - /* * updateTargetListEntry() * This is used in UPDATE statements (and ON CONFLICT DO UPDATE) @@ -672,7 +661,7 @@ updateTargetListEntry(ParseState *pstate, * might want to decorate indirection cells with their own location info, * in which case the location argument could probably be dropped.) */ -static Node * +Node * transformAssignmentIndirection(ParseState *pstate, Node *basenode, const char *targetName, diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index da5ede866c..00af37a4a9 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -108,6 +108,7 @@ typedef struct List *views; /* CREATE VIEW items */ List *indexes; /* CREATE INDEX items */ List *triggers; /* CREATE TRIGGER items */ + List *variables; /* CREATE VARIABLE items */ List *grants; /* GRANT items */ } CreateSchemaStmtContext; @@ -3438,6 +3439,7 @@ transformCreateSchemaStmt(CreateSchemaStmt *stmt) cxt.views = NIL; cxt.indexes = NIL; cxt.triggers = NIL; + cxt.variables = NIL; cxt.grants = NIL; /* @@ -3503,6 +3505,14 @@ transformCreateSchemaStmt(CreateSchemaStmt *stmt) } break; + case T_CreateSchemaVarStmt: + { + CreateSchemaVarStmt *elp = (CreateSchemaVarStmt *) element; + + setSchemaName(cxt.schemaname, &elp->variable->schemaname); + cxt.variables = lappend(cxt.variables, element); + } + case T_GrantStmt: cxt.grants = lappend(cxt.grants, element); break; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 5b87c554f5..2d634c7dd3 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -3342,7 +3342,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events) rt_entry_relation, parsetree->resultRelation, NULL); } - else if (event == CMD_DELETE) + else if (event == CMD_DELETE || event == CMD_LET) { /* Nothing to do here */ } diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 66cc5c35c6..34ddb79a3d 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -193,6 +193,10 @@ ProcessQuery(PlannedStmt *plan, "DELETE " UINT64_FORMAT, queryDesc->estate->es_processed); break; + case CMD_LET: + snprintf(completionTag, COMPLETION_TAG_BUFSIZE, + "LET "); + break; default: strcpy(completionTag, "???"); break; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 287addf429..00965ca506 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -47,6 +47,7 @@ #include "commands/proclang.h" #include "commands/publicationcmds.h" #include "commands/schemacmds.h" +#include "commands/schemavar.h" #include "commands/seclabel.h" #include "commands/sequence.h" #include "commands/subscriptioncmds.h" @@ -113,6 +114,7 @@ CommandIsReadOnly(PlannedStmt *pstmt) case CMD_DELETE: return false; case CMD_UTILITY: + case CMD_LET: /* For now, treat all utility commands as read/write */ return false; default: @@ -178,6 +180,7 @@ check_xact_readonly(Node *parsetree) case T_CreateSchemaStmt: case T_CreateSeqStmt: case T_CreateStmt: + case T_CreateSchemaVarStmt: case T_CreateTableAsStmt: case T_RefreshMatViewStmt: case T_CreateTableSpaceStmt: @@ -1459,6 +1462,10 @@ ProcessUtilitySlow(ParseState *pstate, address = AlterSequence(pstate, (AlterSeqStmt *) parsetree); break; + case T_CreateSchemaVarStmt: + address = DefineSchemaVariable(pstate, (CreateSchemaVarStmt *) parsetree); + break; + case T_CreateTableAsStmt: address = ExecCreateTableAs((CreateTableAsStmt *) parsetree, queryString, params, queryEnv, @@ -2032,6 +2039,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_STATISTIC_EXT: tag = "ALTER STATISTICS"; break; + case OBJECT_VARIABLE: + tag = "ALTER VARIABLE"; + break; default: tag = "???"; break; @@ -2081,6 +2091,10 @@ CreateCommandTag(Node *parsetree) tag = "SELECT"; break; + case T_LetStmt: + tag = "LET"; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: { @@ -2245,6 +2259,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_INDEX: tag = "DROP INDEX"; break; + case OBJECT_VARIABLE: + tag = "DROP VARIABLE"; + break; case OBJECT_TYPE: tag = "DROP TYPE"; break; @@ -2499,6 +2516,10 @@ CreateCommandTag(Node *parsetree) tag = "ALTER SEQUENCE"; break; + case T_CreateSchemaVarStmt: + tag = "CREATE VARIABLE"; + break; + case T_DoStmt: tag = "DO"; break; @@ -2616,6 +2637,9 @@ CreateCommandTag(Node *parsetree) case DISCARD_SEQUENCES: tag = "DISCARD SEQUENCES"; break; + case DISCARD_VARIABLES: + tag = "DISCARD VARIABLES"; + break; default: tag = "???"; } @@ -2820,6 +2844,9 @@ CreateCommandTag(Node *parsetree) case CMD_DELETE: tag = "DELETE"; break; + case CMD_LET: + tag = "LET"; + break; case CMD_UTILITY: tag = CreateCommandTag(stmt->utilityStmt); break; @@ -2938,6 +2965,10 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_ALL; break; + case T_LetStmt: + lev = LOGSTMT_ALL; + break; + /* utility statements --- same whether raw or cooked */ case T_TransactionStmt: lev = LOGSTMT_ALL; @@ -3391,6 +3422,7 @@ GetCommandLogLevel(Node *parsetree) switch (stmt->commandType) { case CMD_SELECT: + case CMD_LET: lev = LOGSTMT_ALL; break; diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 0cfc297b65..fcd695836a 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -808,6 +808,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_USAGE; owner_default = ACL_ALL_RIGHTS_TYPE; break; + case OBJECT_VARIABLE: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_VARIABLE; + break; default: elog(ERROR, "unrecognized objtype: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 74e1cd8afb..5d3d7451b8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -40,6 +40,7 @@ #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablespace.h" +#include "commands/schemavar.h" #include "common/keywords.h" #include "executor/spi.h" #include "funcapi.h" @@ -382,6 +383,7 @@ static void get_update_query_targetlist_def(Query *query, List *targetList, deparse_context *context, RangeTblEntry *rte); static void get_delete_query_def(Query *query, deparse_context *context); +static void get_let_query_def(Query *query, deparse_context *context); static void get_utility_query_def(Query *query, deparse_context *context); static void get_basic_select_query(Query *query, deparse_context *context, TupleDesc resultDesc); @@ -4989,6 +4991,10 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace, get_delete_query_def(query, &context); break; + case CMD_LET: + get_let_query_def(query, &context); + break; + case CMD_NOTHING: appendStringInfoString(buf, "NOTHING"); break; @@ -6206,6 +6212,58 @@ get_insert_query_def(Query *query, deparse_context *context) } } +/* ---------- + * get_let_query_def - Parse back an LET parsetree + * ---------- + */ +static void +get_let_query_def(Query *query, deparse_context *context) +{ + StringInfo buf = context->buf; + RangeTblEntry *select_rte = NULL; + RangeTblEntry *rte; + ListCell *l; + + /* + * If it's an INSERT ... SELECT or multi-row VALUES, there will be a + * single RTE for the SELECT or VALUES. Plain VALUES has neither. + */ + foreach(l, query->rtable) + { + rte = (RangeTblEntry *) lfirst(l); + + if (rte->rtekind == RTE_SUBQUERY) + { + if (select_rte) + elog(ERROR, "too many subquery RTEs in INSERT"); + select_rte = rte; + } + } + + /* + * Start the query with INSERT INTO relname + */ + rte = rt_fetch(query->resultRelation, query->rtable); + Assert(rte->rtekind == RTE_RELATION); + + if (PRETTY_INDENT(context)) + { + context->indentLevel += PRETTYINDENT_STD; + appendStringInfoChar(buf, ' '); + } + appendStringInfo(buf, "LET %s ", + generate_relation_name(rte->relid, NIL)); + + appendStringInfo(buf, " = "); + + if (select_rte) + { + /* Add the SELECT */ + get_query_def(select_rte->subquery, buf, NIL, NULL, + context->prettyFlags, context->wrapColumn, + context->indentLevel); + } +} /* ---------- * get_update_query_def - Parse back an UPDATE parsetree @@ -7281,6 +7339,13 @@ get_parameter(Param *param, deparse_context *context) deparse_namespace *dpns; ListCell *ancestor_cell; + if (param->paramkind == PARAM_SCHEMA_VARIABLE) + { + appendStringInfo(context->buf, "%s", get_schemavar_name(param->paramid)); + + return; + } + /* * If it's a PARAM_EXEC parameter, try to locate the expression from which * the parameter was computed. Note that failing to find a referent isn't diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index 0d147cb08d..97d6607239 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -326,7 +326,8 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables, /* Some kinds never have parents */ if (tblinfo[i].relkind == RELKIND_SEQUENCE || tblinfo[i].relkind == RELKIND_VIEW || - tblinfo[i].relkind == RELKIND_MATVIEW) + tblinfo[i].relkind == RELKIND_MATVIEW || + tblinfo[i].relkind == RELKIND_VARIABLE) continue; /* @@ -475,7 +476,8 @@ flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables) /* Some kinds never have parents */ if (tbinfo->relkind == RELKIND_SEQUENCE || tbinfo->relkind == RELKIND_VIEW || - tbinfo->relkind == RELKIND_MATVIEW) + tbinfo->relkind == RELKIND_MATVIEW || + tbinfo->relkind == RELKIND_VARIABLE) continue; /* Don't bother computing anything for non-target tables, either */ diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 83c976eaf7..70b3da9c93 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3480,7 +3480,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te, ArchiveHandle *AH) strcmp(type, "SERVER") == 0 || strcmp(type, "PUBLICATION") == 0 || strcmp(type, "SUBSCRIPTION") == 0 || - strcmp(type, "USER MAPPING") == 0) + strcmp(type, "USER MAPPING") == 0 || + strcmp(type, "VARIABLE") == 0) { appendPQExpBuffer(buf, "%s ", type); if (te->namespace && *te->namespace) @@ -3661,6 +3662,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData) strcmp(te->desc, "TABLE") == 0 || strcmp(te->desc, "TYPE") == 0 || strcmp(te->desc, "VIEW") == 0 || + strcmp(te->desc, "VARIABLE") == 0 || strcmp(te->desc, "MATERIALIZED VIEW") == 0 || strcmp(te->desc, "SEQUENCE") == 0 || strcmp(te->desc, "FOREIGN TABLE") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index c5b49459cc..046f2c7a96 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -1325,10 +1325,11 @@ expand_table_name_patterns(Archive *fout, "\n LEFT JOIN pg_catalog.pg_namespace n" "\n ON n.oid OPERATOR(pg_catalog.=) c.relnamespace" "\nWHERE c.relkind OPERATOR(pg_catalog.=) ANY" - "\n (array['%c', '%c', '%c', '%c', '%c', '%c'])\n", + "\n (array['%c', '%c', '%c', '%c', '%c', '%c, %c'])\n", RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW, RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE, - RELKIND_PARTITIONED_TABLE); + RELKIND_PARTITIONED_TABLE, + RELKIND_VARIABLE); processSQLNamePattern(GetConnection(fout), query, cell->val, true, false, "n.nspname", "c.relname", NULL, "pg_catalog.pg_table_is_visible(c.oid)"); @@ -2256,6 +2257,10 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo, bool oids) dopt->no_unlogged_table_data) return; + /* Don't dump data of variables */ + if (tbinfo->relkind == RELKIND_VARIABLE) + return; + /* Check that the data is not explicitly excluded */ if (simple_oid_list_member(&tabledata_exclude_oids, tbinfo->dobj.catId.oid)) @@ -2477,9 +2482,10 @@ guessConstraintInheritance(TableInfo *tblinfo, int numTables) TableInfo **parents; TableInfo *parent; - /* Sequences and views never have parents */ + /* Sequences, variables and views never have parents */ if (tbinfo->relkind == RELKIND_SEQUENCE || - tbinfo->relkind == RELKIND_VIEW) + tbinfo->relkind == RELKIND_VIEW || + tbinfo->relkind == RELKIND_VARIABLE) continue; /* Don't bother computing anything for non-target tables, either */ @@ -5989,7 +5995,7 @@ getTables(Archive *fout, int *numTables) "(c.oid = pip.objoid " "AND pip.classoid = 'pg_class'::regclass " "AND pip.objsubid = 0) " - "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c', '%c') " + "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c', '%c', '%c') " "ORDER BY c.oid", acl_subquery->data, racl_subquery->data, @@ -6008,7 +6014,8 @@ getTables(Archive *fout, int *numTables) RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW, RELKIND_COMPOSITE_TYPE, RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE, - RELKIND_PARTITIONED_TABLE); + RELKIND_PARTITIONED_TABLE, + RELKIND_VARIABLE); destroyPQExpBuffer(acl_subquery); destroyPQExpBuffer(racl_subquery); @@ -15416,6 +15423,11 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) srvname = NULL; ftoptions = NULL; break; + case RELKIND_VARIABLE: + reltypename = "VARIABLE"; + srvname = NULL; + ftoptions = NULL; + break; default: reltypename = "TABLE"; srvname = NULL; @@ -15437,12 +15449,28 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) reltypename, qualrelname); + /* + * Binary upgrade of variables should to be fixed with support + * composite types, and should be tested ToDo + */ + if (tbinfo->relkind == RELKIND_VARIABLE) + { + appendPQExpBuffer(q, " AS"); + if (tbinfo->reloftype != NULL) + appendPQExpBuffer(q, " %s", + tbinfo->atttypnames[0]); + if (tbinfo->attrdefs[0] != NULL && tbinfo->attrdefs[0]->adef_expr != NULL) + appendPQExpBuffer(q, " DEFAULT %s", + tbinfo->attrdefs[0]->adef_expr); + } + /* * Attach to type, if reloftype; except in case of a binary upgrade, * we dump the table normally and attach it to the type afterward. */ - if (tbinfo->reloftype && !dopt->binary_upgrade) - appendPQExpBuffer(q, " OF %s", tbinfo->reloftype); + else if (tbinfo->reloftype && !dopt->binary_upgrade) + appendPQExpBuffer(q, " OF %s", + tbinfo->reloftype); /* * If the table is a partition, dump it as such; except in the case of @@ -15465,7 +15493,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) fmtQualifiedDumpable(parentRel)); } - if (tbinfo->relkind != RELKIND_MATVIEW) + if (tbinfo->relkind != RELKIND_MATVIEW && + !(tbinfo->relkind == RELKIND_VARIABLE && tbinfo->reloftype != NULL)) { /* Dump the attributes */ actual_atts = 0; @@ -15591,7 +15620,8 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo) * We must have a parenthesized attribute list, even though * empty, when not using the OF TYPE or PARTITION OF syntax. */ - appendPQExpBufferStr(q, " (\n)"); + if (tbinfo->relkind != RELKIND_VARIABLE) + appendPQExpBufferStr(q, " (\n)"); } if (tbinfo->ispartition && !dopt->binary_upgrade) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index e96c662b1e..78eb878cfc 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -84,7 +84,7 @@ typedef enum DO_POLICY, DO_PUBLICATION, DO_PUBLICATION_REL, - DO_SUBSCRIPTION + DO_SUBSCRIPTION, } DumpableObjectType; /* component types of an object which can be selected for dumping */ diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 81cd65ee8d..ddf78cc9d2 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -2287,6 +2287,75 @@ my %tests = ( { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, unlike => { exclude_dump_test_schema => 1, }, }, + 'CREATE VARIABLE test_variable' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable AS integer;', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable AS integer;\E/xm, + like => { + binary_upgrade => 1, + clean => 1, + clean_if_exists => 1, + createdb => 1, + defaults => 1, + exclude_test_table => 1, + exclude_test_table_data => 1, + no_blobs => 1, + no_privs => 1, + no_owner => 1, + only_dump_test_schema => 1, + pg_dumpall_dbprivs => 1, + schema_only => 1, + section_pre_data => 1, + test_schema_plus_blobs => 1, + with_oids => 1, }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_test_table => 1, + pg_dumpall_globals => 1, + pg_dumpall_globals_clean => 1, + role => 1, + section_post_data => 1, }, }, + + 'CREATE VARIABLE test_variable with field list' => { + all_runs => 1, + catch_all => 'CREATE ... commands', + create_order => 61, + create_sql => 'CREATE VARIABLE dump_test.variable1 AS (a int, b numeric(10,2));', + regexp => qr/^ + \QCREATE VARIABLE dump_test.variable1 AS (\E + \n\s+\Qa integer,\E + \n\s+\Qb numeric(10,2)\E + \n\); + /xm, + like => { + binary_upgrade => 1, + clean => 1, + clean_if_exists => 1, + createdb => 1, + defaults => 1, + exclude_test_table => 1, + exclude_test_table_data => 1, + no_blobs => 1, + no_privs => 1, + no_owner => 1, + only_dump_test_schema => 1, + pg_dumpall_dbprivs => 1, + schema_only => 1, + section_pre_data => 1, + test_schema_plus_blobs => 1, + with_oids => 1, }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_test_table => 1, + pg_dumpall_globals => 1, + pg_dumpall_globals_clean => 1, + role => 1, + section_post_data => 1, }, }, + + 'ALTER VIEW test_view SET DEFAULT' => { create_order => 62, create_sql => diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 4c85f43f09..63411a5fa4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -794,6 +794,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) case 'i': case 's': case 'E': + case 'V': success = listTables(&cmd[1], pattern, show_verbose, show_system); break; case 'r': diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 410131e5c7..b193d3ccf8 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1828,6 +1828,10 @@ describeOneTableDetails(const char *schemaname, printfPQExpBuffer(&title, _("Foreign table \"%s.%s\""), schemaname, relationname); break; + case RELKIND_VARIABLE: + printfPQExpBuffer(&title, _("schema variable \"%s.%s\""), + schemaname, relationname); + break; case RELKIND_PARTITIONED_TABLE: if (tableinfo.relpersistence == 'u') printfPQExpBuffer(&title, _("Unlogged table \"%s.%s\""), @@ -3401,6 +3405,7 @@ listDbRoleSettings(const char *pattern, const char *pattern2) * m - materialized views * s - sequences * E - foreign table (Note: different from 'f', the relkind value) + * V - schema variable * (any order of the above is fine) */ bool @@ -3412,6 +3417,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys bool showMatViews = strchr(tabtypes, 'm') != NULL; bool showSeq = strchr(tabtypes, 's') != NULL; bool showForeign = strchr(tabtypes, 'E') != NULL; + bool showVariables = strchr(tabtypes, 'V') != NULL; PQExpBufferData buf; PGresult *res; @@ -3419,8 +3425,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys static const bool translate_columns[] = {false, false, true, false, false, false, false}; /* If tabtypes is empty, we default to \dtvmsE (but see also command.c) */ - if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign)) - showTables = showViews = showMatViews = showSeq = showForeign = true; + if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign || showVariables)) + showTables = showViews = showMatViews = showSeq = showForeign = showVariables = true; initPQExpBuffer(&buf); @@ -3441,6 +3447,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'" " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'" " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_VARIABLE) " THEN '%s'" " END as \"%s\",\n" " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"", gettext_noop("Schema"), @@ -3454,6 +3461,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys gettext_noop("foreign table"), gettext_noop("table"), /* partitioned table */ gettext_noop("index"), /* partitioned index */ + gettext_noop("schema variable"), gettext_noop("Type"), gettext_noop("Owner")); @@ -3507,6 +3515,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys appendPQExpBufferStr(&buf, "'s',"); /* was RELKIND_SPECIAL */ if (showForeign) appendPQExpBufferStr(&buf, CppAsString2(RELKIND_FOREIGN_TABLE) ","); + if (showVariables) + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_VARIABLE) ","); appendPQExpBufferStr(&buf, "''"); /* dummy */ appendPQExpBufferStr(&buf, ")\n"); diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index a4cc5efae0..c5f107d814 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -60,7 +60,7 @@ extern bool listTSTemplates(const char *pattern, bool verbose); /* \l */ extern bool listAllDbs(const char *pattern, bool verbose); -/* \dt, \di, \ds, \dS, etc. */ +/* \dt, \di, \ds, \dS, \dvar etc. */ extern bool listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSystem); /* \dD */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 702e742af4..2da50f7290 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -167,7 +167,7 @@ slashUsage(unsigned short int pager) * Use "psql --help=commands | wc" to count correctly. It's okay to count * the USE_READLINE line even in builds without that. */ - output = PageOutput(125, pager ? &(pset.popt.topt) : NULL); + output = PageOutput(126, pager ? &(pset.popt.topt) : NULL); fprintf(output, _("General\n")); fprintf(output, _(" \\copyright show PostgreSQL usage and distribution terms\n")); @@ -257,6 +257,7 @@ slashUsage(unsigned short int pager) fprintf(output, _(" \\dT[S+] [PATTERN] list data types\n")); fprintf(output, _(" \\du[S+] [PATTERN] list roles\n")); fprintf(output, _(" \\dv[S+] [PATTERN] list views\n")); + fprintf(output, _(" \\dV[S+] [PATTERN] list schema variables\n")); fprintf(output, _(" \\dx[+] [PATTERN] list extensions\n")); fprintf(output, _(" \\dy [PATTERN] list event triggers\n")); fprintf(output, _(" \\l[+] [PATTERN] list databases\n")); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index b431efc983..ab14923392 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -805,6 +805,22 @@ static const SchemaQuery Query_for_list_of_statistics = { NULL }; +static const SchemaQuery Query_for_list_of_variables = { + /* min_server_version */ + 0, + /* catname */ + "pg_catalog.pg_class c", + /* selcondition */ + "c.relkind IN ('V')", + /* viscondition */ + "pg_catalog.pg_table_is_visible(c.oid)", + /* namespace */ + "c.relnamespace", + /* result */ + "pg_catalog.quote_ident(c.relname)", + /* qualresult */ + NULL +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1249,6 +1265,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles " UNION SELECT 'MAPPING FOR'"}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1604,7 +1621,7 @@ psql_completion(const char *text, int start, int end) "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", "INSERT", "LISTEN", "LOAD", "LOCK", + "FETCH", "GRANT", "IMPORT", "INSERT", "LET", "LISTEN", "LOAD", "LOCK", "MOVE", "NOTIFY", "PREPARE", "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "RESET", "REVOKE", "ROLLBACK", @@ -1623,7 +1640,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\dRs", "\\dRp", "\\ds", "\\dS", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy", "\\dvar", "\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\errverbose", "\\ev", "\\f", @@ -2835,6 +2852,14 @@ psql_completion(const char *text, int start, int end) else if (Matches4("CREATE", "ROLE|USER|GROUP", MatchAny, "IN")) COMPLETE_WITH_LIST2("GROUP", "ROLE"); +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE with AS */ + else if (TailMatches3("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH_CONST("AS"); + /* Complete CREATE VARIABLE with AS types*/ + else if (TailMatches4("CREATE", "VARIABLE", MatchAny, "AS")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes, NULL); + /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE VIEW with AS */ else if (TailMatches3("CREATE", "VIEW", MatchAny)) @@ -2990,6 +3015,12 @@ psql_completion(const char *text, int start, int end) else if (Matches5("DROP", "RULE", MatchAny, "ON", MatchAny)) COMPLETE_WITH_LIST2("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches2("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL); + else if (Matches3("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH_LIST2("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches1("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -3000,14 +3031,14 @@ psql_completion(const char *text, int start, int end) * Complete EXPLAIN [ANALYZE] [VERBOSE] with list of EXPLAIN-able commands */ else if (Matches1("EXPLAIN")) - COMPLETE_WITH_LIST7("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", - "ANALYZE", "VERBOSE"); + COMPLETE_WITH_LIST8("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", + "ANALYZE", "VERBOSE", "LET"); else if (Matches2("EXPLAIN", "ANALYZE")) - COMPLETE_WITH_LIST6("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", - "VERBOSE"); + COMPLETE_WITH_LIST7("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", + "VERBOSE", "LET"); else if (Matches2("EXPLAIN", "VERBOSE") || Matches3("EXPLAIN", "ANALYZE", "VERBOSE")) - COMPLETE_WITH_LIST5("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE"); + COMPLETE_WITH_LIST6("SELECT", "INSERT", "DELETE", "UPDATE", "DECLARE", "LET"); /* FETCH && MOVE */ /* Complete FETCH with one of FORWARD, BACKWARD, RELATIVE */ @@ -3116,6 +3147,7 @@ psql_completion(const char *text, int start, int end) " UNION SELECT 'ALL ROUTINES IN SCHEMA'" " UNION SELECT 'ALL SEQUENCES IN SCHEMA'" " UNION SELECT 'ALL TABLES IN SCHEMA'" + " UNION SELECT 'ALL VARIABLES IN SCHEMA'" " UNION SELECT 'DATABASE'" " UNION SELECT 'DOMAIN'" " UNION SELECT 'FOREIGN DATA WRAPPER'" @@ -3129,14 +3161,16 @@ psql_completion(const char *text, int start, int end) " UNION SELECT 'SEQUENCE'" " UNION SELECT 'TABLE'" " UNION SELECT 'TABLESPACE'" - " UNION SELECT 'TYPE'"); + " UNION SELECT 'TYPE'" + " UNION SELECT 'VARIABLE'"); } else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "ALL")) - COMPLETE_WITH_LIST5("FUNCTIONS IN SCHEMA", + COMPLETE_WITH_LIST6("FUNCTIONS IN SCHEMA", "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches4("GRANT|REVOKE", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH_LIST2("DATA WRAPPER", "SERVER"); @@ -3166,6 +3200,8 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_sequences, NULL); else if (TailMatches1("TABLE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tsvmf, NULL); + else if (TailMatches1("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL); else if (TailMatches1("TABLESPACE")) COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches1("TYPE")) @@ -3322,7 +3358,7 @@ psql_completion(const char *text, int start, int end) /* PREPARE xx AS */ else if (Matches3("PREPARE", MatchAny, "AS")) - COMPLETE_WITH_LIST4("SELECT", "UPDATE", "INSERT", "DELETE FROM"); + COMPLETE_WITH_LIST5("SELECT", "UPDATE", "INSERT", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -3545,6 +3581,14 @@ psql_completion(const char *text, int start, int end) else if (TailMatches4("UPDATE", MatchAny, "SET", MatchAny)) COMPLETE_WITH_CONST("="); +/* LET --- can be inside EXPLAIN, PREPARE etc */ + /* If prev. word is LET suggest a list of variables */ + else if (TailMatches1("LET")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables, NULL); + /* Complete LET with "=" */ + else if (TailMatches2("LET", MatchAny)) + COMPLETE_WITH_CONST("="); + /* USER MAPPING */ else if (Matches3("ALTER|CREATE|DROP", "USER", "MAPPING")) COMPLETE_WITH_CONST("FOR"); diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index dc6c415c58..04a5849056 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -104,6 +104,7 @@ typedef FormData_pg_class *Form_pg_class; #define RELKIND_FOREIGN_TABLE 'f' /* foreign table */ #define RELKIND_PARTITIONED_TABLE 'p' /* partitioned table */ #define RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */ +#define RELKIND_VARIABLE 'V' /* schema variable */ #define RELPERSISTENCE_PERMANENT 'p' /* regular table */ #define RELPERSISTENCE_UNLOGGED 'u' /* unlogged permanent table */ diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index d0410f5586..56deef1a45 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -57,6 +57,7 @@ typedef FormData_pg_default_acl *Form_pg_default_acl; #define DEFACLOBJ_FUNCTION 'f' /* function */ #define DEFACLOBJ_TYPE 'T' /* type */ #define DEFACLOBJ_NAMESPACE 'n' /* namespace */ +#define DEFACLOBJ_VARIABLE 'V' /* variable */ #endif /* EXPOSE_TO_CLIENT_CODE */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 66c6c224a8..08e38c6186 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10202,4 +10202,18 @@ proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any', proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' }, +#schema variables function interface +{ oid => '6122', descr => 'returns value of schema variable', + proname => 'get_schema_variable', proisstrict => 'f', prorettype => 'anyelement', + proargtypes => 'regclass anyelement', prosrc => 'get_schema_variable' }, +{ oid => '6123', descr => 'set value of schema variable', + proname => 'set_schema_variable', proisstrict => 'f', prorettype => 'void', + proargtypes => 'regclass anyelement', prosrc => 'set_schema_variable' }, +{ oid => '6124', descr => 'get value of field of schema variable of composite type', + proname => 'get_schema_variable', proisstrict => 'f', prorettype => 'anyelement', + proargtypes => 'regclass name anyelement', prosrc => 'get_schema_variable_field' }, +{ oid => '6125', descr => 'set value of field of schema variable of composite type', + proname => 'set_schema_variable', proisstrict => 'f', prorettype => 'void', + proargtypes => 'regclass name anyelement', prosrc => 'set_schema_variable_field' }, + ] diff --git a/src/include/commands/schemavar.h b/src/include/commands/schemavar.h new file mode 100644 index 0000000000..fcd516d192 --- /dev/null +++ b/src/include/commands/schemavar.h @@ -0,0 +1,39 @@ +/*------------------------------------------------------------------------- + * + * schemavar.h + * prototypes for schemavar.c. + * + * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/schemavar.h + * + *------------------------------------------------------------------------- + */ +#ifndef SCHEMAVAR_H +#define SCHEMAVAR_H + +#include "catalog/objectaddress.h" +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" + +extern ObjectAddress DefineSchemaVariable(ParseState *pstate, CreateSchemaVarStmt *var); + +extern void ResetSchemaVariablesCache(void); + +extern char *get_schemavar_name(Oid varid); + +extern void SetSchemaVariable(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod); +extern Datum GetSchemaVariable(Oid varid, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval); +extern void SetSchemaVariableSecure(Oid varid, Datum value, bool isNull, Oid typid, int32 typmod); +extern Datum GetSchemaVariableSecure(Oid varid, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval); + +extern void SetSchemaVariableField(Oid varid, const char *fieldname, Datum value, bool isNull, Oid typid, int32 typmod); +extern Datum GetSchemaVariableField(Oid varid, const char *fieldname, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval); +extern void SetSchemaVariableFieldSecure(Oid varid, const char *fieldname, Datum value, bool isNull, Oid typid, int32 typmod); +extern Datum GetSchemaVariableFieldSecure(Oid varid, const char *fieldname, bool *isNull, Oid typid, int32 typmod, int16 typlen, bool typbyval); + +extern void schemavar_get_typ_typmod(Oid varid, Oid *typ, int32 *typmod); +extern bool is_schemavar(Oid varid, bool *is_rowtype); + +#endif \ No newline at end of file diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index f7b1f77616..58d282dc92 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -138,6 +138,7 @@ typedef enum ExprEvalOp EEOP_PARAM_EXEC, EEOP_PARAM_EXTERN, EEOP_PARAM_CALLBACK, + EEOP_PARAM_SCHEMA_VARIABLE, /* return CaseTestExpr value */ EEOP_CASE_TESTVAL, @@ -344,11 +345,14 @@ typedef struct ExprEvalStep TupleDesc argdesc; } nulltest_row; - /* for EEOP_PARAM_EXEC/EXTERN */ + /* for EEOP_PARAM_EXEC/EXTERN/VARIABLE */ struct { - int paramid; /* numeric ID for parameter */ - Oid paramtype; /* OID of parameter's datatype */ + int paramid; /* numeric ID for parameter */ + Oid paramtype; /* OID of parameter's datatype */ + int32 paramtypmod; /* typmod of param (not used yet) */ + int16 paramtyplen; /* expected length */ + bool paramtypbyval; /* is passed by value */ } param; /* for EEOP_PARAM_CALLBACK */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index adb159a6da..806b58f6ca 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -348,6 +348,7 @@ typedef enum NodeTag T_CreateTableAsStmt, T_CreateSeqStmt, T_AlterSeqStmt, + T_CreateSchemaVarStmt, T_VariableSetStmt, T_VariableShowStmt, T_DiscardStmt, @@ -419,6 +420,7 @@ typedef enum NodeTag T_CreateStatsStmt, T_AlterCollationStmt, T_CallStmt, + T_LetStmt, /* * TAGS FOR PARSE TREE NODES (parsenodes.h) @@ -661,6 +663,7 @@ typedef enum CmdType CMD_UPDATE, /* update stmt */ CMD_INSERT, /* insert stmt */ CMD_DELETE, + CMD_LET, CMD_UTILITY, /* cmds like create, destroy, copy, vacuum, * etc. */ CMD_NOTHING /* dummy command for instead nothing rules diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 6390f7e8c1..46ba22f10c 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1505,6 +1505,15 @@ typedef struct UpdateStmt WithClause *withClause; /* WITH clause */ } UpdateStmt; +typedef struct LetStmt +{ + NodeTag type; + List *target; /* list of fields */ + Node *selectStmt; /* the source SELECT/VALUES, or NULL */ + int location; +} LetStmt; + + /* ---------------------- * Select Statement * @@ -1682,6 +1691,7 @@ typedef enum ObjectType OBJECT_TSTEMPLATE, OBJECT_TYPE, OBJECT_USER_MAPPING, + OBJECT_VARIABLE, OBJECT_VIEW } ObjectType; @@ -2497,6 +2507,20 @@ typedef struct AlterSeqStmt bool missing_ok; /* skip error if a role is missing? */ } AlterSeqStmt; +/* ---------------------- + * Create VARIABLE Statement + * ---------------------- + */ +typedef struct CreateSchemaVarStmt +{ + NodeTag type; + RangeVar *variable; /* the variable to create */ + TypeName *typeName; /* the scalar variable type */ + Node *defexpr; /* default expression */ + List *fields; /* the fields of composite variable type */ + bool if_not_exists; /* just do nothing if it already exists? */ +} CreateSchemaVarStmt; + /* ---------------------- * Create {Aggregate|Operator|Type} Statement * ---------------------- @@ -3231,7 +3255,8 @@ typedef enum DiscardMode DISCARD_ALL, DISCARD_PLANS, DISCARD_SEQUENCES, - DISCARD_TEMP + DISCARD_TEMP, + DISCARD_VARIABLES } DiscardMode; typedef struct DiscardStmt diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index f90aa7b2a1..44af7c3fbd 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -230,13 +230,17 @@ 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_SCHEMA_VARIABLE: The parameter is a access to schema variable + * paramid holds varid. */ typedef enum ParamKind { PARAM_EXTERN, PARAM_EXEC, PARAM_SUBLINK, - PARAM_MULTIEXPR + PARAM_MULTIEXPR, + PARAM_SCHEMA_VARIABLE } ParamKind; typedef struct Param diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 23db40147b..d3ed3f4d0f 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -231,6 +231,7 @@ PG_KEYWORD("leading", LEADING, RESERVED_KEYWORD) PG_KEYWORD("leakproof", LEAKPROOF, UNRESERVED_KEYWORD) PG_KEYWORD("least", LEAST, COL_NAME_KEYWORD) PG_KEYWORD("left", LEFT, TYPE_FUNC_NAME_KEYWORD) +PG_KEYWORD("let", LET, UNRESERVED_KEYWORD) PG_KEYWORD("level", LEVEL, UNRESERVED_KEYWORD) PG_KEYWORD("like", LIKE, TYPE_FUNC_NAME_KEYWORD) PG_KEYWORD("limit", LIMIT, RESERVED_KEYWORD) @@ -434,6 +435,8 @@ PG_KEYWORD("validator", VALIDATOR, UNRESERVED_KEYWORD) PG_KEYWORD("value", VALUE_P, UNRESERVED_KEYWORD) PG_KEYWORD("values", VALUES, COL_NAME_KEYWORD) PG_KEYWORD("varchar", VARCHAR, COL_NAME_KEYWORD) +PG_KEYWORD("variable", VARIABLE, UNRESERVED_KEYWORD) +PG_KEYWORD("variables", VARIABLES, UNRESERVED_KEYWORD) PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 0230543810..d25c061088 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -135,6 +135,8 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, * to process them like UPDATE. (Note this can change intra-statement, for * cases like INSERT ON CONFLICT UPDATE.) * + * p_is_let: true to process assignment expressions like LET. + * * p_windowdefs: list of WindowDefs representing WINDOW and OVER clauses. * We collect these while transforming expressions and then transform them * afterwards (so that any resjunk tlist items needed for the sort/group @@ -184,6 +186,7 @@ struct ParseState Relation p_target_relation; /* INSERT/UPDATE/DELETE target rel */ RangeTblEntry *p_target_rangetblentry; /* target rel's RTE */ bool p_is_insert; /* process assignment like INSERT not UPDATE */ + bool p_is_let; /* process assignment LET stmt */ List *p_windowdefs; /* raw representations of window clauses */ ParseExprKind p_expr_kind; /* what kind of expression we're parsing */ int p_next_resno; /* next targetlist resno to assign */ diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index b9792acdae..c894147d06 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -129,4 +129,10 @@ extern Oid attnumTypeId(Relation rd, int attid); extern Oid attnumCollationId(Relation rd, int attid); extern bool isQueryUsingTempRelation(Query *query); +extern RangeVar *makeRangeVarForTargetOfSchemaVariable(ParseState *pstate, + char *field1, char *field2, char *field3, + int location, char **fieldname, int *fieldname_pos, bool noerror); + +extern Node *toSchemaVariable(ParseState *pstate, char *nspname, char *varname, char *fieldname, int location); + #endif /* PARSE_RELATION_H */ diff --git a/src/include/parser/parse_target.h b/src/include/parser/parse_target.h index ec6e0c102f..72eb3cd7b6 100644 --- a/src/include/parser/parse_target.h +++ b/src/include/parser/parse_target.h @@ -43,4 +43,16 @@ extern TupleDesc expandRecordVariable(ParseState *pstate, Var *var, extern char *FigureColname(Node *node); extern char *FigureIndexColname(Node *node); +extern Node *transformAssignmentIndirection(ParseState *pstate, + Node *basenode, + const char *targetName, + bool targetIsArray, + Oid targetTypeId, + int32 targetTypMod, + Oid targetCollation, + ListCell *indirection, + Node *rhs, + int location); + + #endif /* PARSE_TARGET_H */ diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index f4d4be8d0d..d0737a9e4b 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -166,6 +166,7 @@ typedef ArrayType Acl; #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) +#define ACL_ALL_RIGHTS_VARIABLE (ACL_SELECT|ACL_UPDATE) /* operation codes for pg_*_aclmask */ typedef enum diff --git a/src/test/regress/expected/schema_variables.out b/src/test/regress/expected/schema_variables.out new file mode 100644 index 0000000000..ab1d3ac66d --- /dev/null +++ b/src/test/regress/expected/schema_variables.out @@ -0,0 +1,456 @@ +CREATE VARIABLE var1 AS integer; +CREATE TEMP VARIABLE var2 AS text; +DROP VARIABLE var1, var2; +-- functional interface +CREATE VARIABLE var1 AS numeric; +SELECT set_schema_variable('var1', 333); + set_schema_variable +--------------------- + +(1 row) + +SELECT get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 333 +(1 row) + +SELECT set_schema_variable('var1', 333::integer); + set_schema_variable +--------------------- + +(1 row) + +SELECT get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 333 +(1 row) + +SELECT set_schema_variable('var1', '333.55'::text); + set_schema_variable +--------------------- + +(1 row) + +SELECT get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 333.55 +(1 row) + +SELECT get_schema_variable('var1', null::int); + get_schema_variable +--------------------- + 334 +(1 row) + +SELECT get_schema_variable('var1', null::text); + get_schema_variable +--------------------- + 333.55 +(1 row) + +-- access rights test +CREATE ROLE var_test_role; +SET ROLE TO var_test_role; +-- should to fail +SELECT set_schema_variable('var1', '1000'::text); +ERROR: permission denied for schema variable var1 +SELECT get_schema_variable('var1', null::numeric); +ERROR: permission denied for schema variable var1 +SET ROLE TO DEFAULT; +GRANT SELECT ON VARIABLE var1 TO var_test_role; +SET ROLE TO var_test_role; +-- should to fail +SELECT set_schema_variable('var1', '1000'::text); +ERROR: permission denied for schema variable var1 +-- should to work +SELECT get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 333.55 +(1 row) + +SET ROLE TO DEFAULT; +GRANT UPDATE ON VARIABLE var1 TO var_test_role; +SET ROLE TO var_test_role; +-- should to work +SELECT set_schema_variable('var1', '1000'::text); + set_schema_variable +--------------------- + +(1 row) + +SELECT get_schema_variable('var1', null::numeric); + get_schema_variable +--------------------- + 1000 +(1 row) + +SET ROLE TO DEFAULT; +REVOKE ALL ON VARIABLE var1 FROM var_test_role; +CREATE VARIABLE var AS integer; +SELECT set_schema_variable('public.var', 1234); + set_schema_variable +--------------------- + +(1 row) + +SELECT public.var; + var +------ + 1234 +(1 row) + +DO $$ +BEGIN + RAISE NOTICE 'public.var is = %', public.var; +END; +$$; +NOTICE: public.var is = 1234 +CREATE OR REPLACE FUNCTION secure_var() +RETURNS int AS $$ + SELECT public.var; +$$ LANGUAGE sql SECURITY DEFINER; +SELECT secure_var(); + secure_var +------------ + 1234 +(1 row) + +SET ROLE TO var_test_role; +-- should to fail +SELECT public.var; +ERROR: permission denied for schema variable var +-- should to work; +SELECT secure_var(); + secure_var +------------ + 1234 +(1 row) + +SET ROLE TO DEFAULT; +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var; + QUERY PLAN +----------------------------------------------- + Function Scan on pg_catalog.generate_series g + Output: v + Function Call: generate_series(1, 100) + Filter: (g.v = var) +(4 rows) + +CREATE VIEW schema_var_view AS SELECT var; +SELECT * FROM schema_var_view; + var +------ + 1234 +(1 row) + +\c - +-- should to work still, but var will be empty +SELECT * FROM schema_var_view; + var +----- + +(1 row) + +LET var1 = pi(); +SELECT var1; + var1 +------------------ + 3.14159265358979 +(1 row) + +-- we can look on execution plan +EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi(); + QUERY PLAN +---------------------------------- + Let on public.var1 + -> Result + Output: 3.14159265358979 +(3 rows) + +-- LET can be prepared +PREPARE var_pp(int, numeric) AS LET var1 = $1 + $2; +EXECUTE var_pp(100, 1.23456); +SELECT var1; + var1 +----------- + 101.23456 +(1 row) + +CREATE VARIABLE var3 AS int; +CREATE OR REPLACE FUNCTION inc(int) +RETURNS int AS $$ +BEGIN + LET public.var3 = COALESCE(public.var3 + $1, $1); + RETURN var3; +END; +$$ LANGUAGE plpgsql; +SELECT inc(1); + inc +----- + 1 +(1 row) + +SELECT inc(1); + inc +----- + 2 +(1 row) + +SELECT inc(1); + inc +----- + 3 +(1 row) + +SELECT inc(1) FROM generate_series(1,10); + inc +----- + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 +(10 rows) + +SET ROLE TO var_test_role; +-- should to fail +LET var3 = 0; +ERROR: permission denied for schema variable var3 +SET ROLE TO DEFAULT; +DROP VIEW schema_var_view; +DROP VARIABLE var CASCADE; +DROP VARIABLE var1 CASCADE; +DROP VARIABLE var3 CASCADE; +-- 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 (x int, y int, z numeric(10,2)); +\d v1 +schema variable "public.v1" + Column | Type +--------+--------------- + x | integer + y | integer + z | numeric(10,2) + +\d v2 +schema variable "public.v2" + Column | Type +--------+--------------- + x | integer + y | integer + z | numeric(10,2) + +-- explicit casting is necessary +LET v1 = (1,2,3.14)::sv_xyz; +LET v2 = (10,20,3.14*10)::v2; +-- should to work too - there are prepared casts +LET v1 = (1,2,3.14)::v1; +SELECT v1; + v1 +------------ + (1,2,3.14) +(1 row) + +SELECT v2; + v2 +--------------- + (10,20,31.40) +(1 row) + +SELECT (v1).*; + x | y | z +---+---+------ + 1 | 2 | 3.14 +(1 row) + +SELECT (v2).*; + x | y | z +----+----+------- + 10 | 20 | 31.40 +(1 row) + +SELECT get_schema_variable('v1', 'z', null::double precision); + get_schema_variable +--------------------- + 3.14 +(1 row) + +SELECT get_schema_variable('v2', 'z', null::double precision); + get_schema_variable +--------------------- + 31.4 +(1 row) + +-- should to fail +SELECT get_schema_variable('v1', 'w', null::double precision); +ERROR: the variable "v1" has not field "w" +SELECT get_schema_variable('v2', 'w', null::double precision); +ERROR: the variable "v2" has not field "w" +CREATE VARIABLE v3 AS integer; +SELECT get_schema_variable('v3', 'w', null::double precision); +ERROR: the variable "v3" is not of row type +SELECT set_schema_variable('v1', 'z', 3.14 * 2); + set_schema_variable +--------------------- + +(1 row) + +SELECT v1; + v1 +------------ + (1,2,6.28) +(1 row) + +SELECT set_schema_variable('v1', 'z', null::double precision); + set_schema_variable +--------------------- + +(1 row) + +SELECT v1; + v1 +-------- + (1,2,) +(1 row) + +SELECT set_schema_variable('v1', 'z', 3.3333::double precision); + set_schema_variable +--------------------- + +(1 row) + +SELECT set_schema_variable('v1', 'x', 10000.22); + set_schema_variable +--------------------- + +(1 row) + +SELECT v1; + v1 +---------------- + (10000,2,3.33) +(1 row) + +SELECT v1.x + v1.z; + ?column? +---------- + 10003.33 +(1 row) + +SELECT v2.x + v2.z; + ?column? +---------- + 41.40 +(1 row) + +-- access to composite fields should be safe too +-- should to fail +SET ROLE TO var_test_role; +SELECT v2.x; +ERROR: permission denied for schema variable v2 +SET ROLE TO DEFAULT; +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP VARIABLE v3; +DROP ROLE var_test_role; +-- scalar variables should not be in conflict with qualified column +CREATE VARIABLE varx AS text; +SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class'; + relname +---------- + pg_class +(1 row) + +-- should to fail +SELECT varx.xxx; +ERROR: missing FROM-clause entry for table "varx" +LINE 1: SELECT varx.xxx; + ^ +-- variables can be updated under RO transaction +BEGIN; +SET TRANSACTION READ ONLY; +LET varx = 'hello'; +COMMIT; +SELECT varx; + varx +------- + hello +(1 row) + +DROP VARIABLE varx; +CREATE VARIABLE v1 AS (a int, b numeric, c text); +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 to fail +LET v1.x = 10; +ERROR: cannot assign to field "x" of column "x" because there is no such column in data type v1 +DROP VARIABLE v1; +-- arrays are supported +CREATE VARIABLE va1 AS numeric[]; +LET va1 = ARRAY[1.1,2.1]; +LET va1[1] = 10.1; +SELECT va1; + va1 +------------ + {10.1,2.1} +(1 row) + +CREATE VARIABLE va2 AS (a numeric, b numeric[]); +LET va2 = (10.1, ARRAY[0.0, 0.0]); +LET va2.a = 10.2; +SELECT va2; + va2 +-------------------- + (10.2,"{0.0,0.0}") +(1 row) + +LET va2.b[1] = 10.3; +SELECT va2; + va2 +--------------------- + (10.2,"{10.3,0.0}") +(1 row) + +DROP VARIABLE va1; +DROP VARIABLE va2; +-- default values +CREATE VARIABLE v1 AS numeric DEFAULT pi(); +LET v1 = v1 * 2; +SELECT v1; + v1 +------------------ + 6.28318530717958 +(1 row) + +CREATE VARIABLE v2 AS (a numeric, b text DEFAULT 'hello'); +LET public.v2.a = pi(); +SELECT v2; + v2 +-------------------------- + (3.14159265358979,hello) +(1 row) + +DROP VARIABLE v1; +DROP VARIABLE v2; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 16f979c8d9..9bf379b87b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -111,7 +111,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # NB: temp.sql does a reconnect which transiently uses 2 connections, # so keep this parallel group to at most 19 tests # ---------- -test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml schema_variables # ---------- # Another group of parallel tests diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index 42632be675..42bf4ecb3f 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -191,3 +191,4 @@ test: partition_aggregate test: event_trigger test: fast_default test: stats +test: schema_variables diff --git a/src/test/regress/sql/schema_variables.sql b/src/test/regress/sql/schema_variables.sql new file mode 100644 index 0000000000..27bbd1fdc3 --- /dev/null +++ b/src/test/regress/sql/schema_variables.sql @@ -0,0 +1,251 @@ +CREATE VARIABLE var1 AS integer; +CREATE TEMP VARIABLE var2 AS text; + +DROP VARIABLE var1, var2; + +-- functional interface +CREATE VARIABLE var1 AS numeric; +SELECT set_schema_variable('var1', 333); +SELECT get_schema_variable('var1', null::numeric); + +SELECT set_schema_variable('var1', 333::integer); +SELECT get_schema_variable('var1', null::numeric); + +SELECT set_schema_variable('var1', '333.55'::text); +SELECT get_schema_variable('var1', null::numeric); +SELECT get_schema_variable('var1', null::int); +SELECT get_schema_variable('var1', null::text); + +-- access rights test + +CREATE ROLE var_test_role; + +SET ROLE TO var_test_role; + +-- should to fail +SELECT set_schema_variable('var1', '1000'::text); +SELECT get_schema_variable('var1', null::numeric); + +SET ROLE TO DEFAULT; + +GRANT SELECT ON VARIABLE var1 TO var_test_role; + +SET ROLE TO var_test_role; +-- should to fail +SELECT set_schema_variable('var1', '1000'::text); +-- should to work +SELECT get_schema_variable('var1', null::numeric); + +SET ROLE TO DEFAULT; + +GRANT UPDATE ON VARIABLE var1 TO var_test_role; + +SET ROLE TO var_test_role; + +-- should to work +SELECT set_schema_variable('var1', '1000'::text); +SELECT get_schema_variable('var1', null::numeric); + +SET ROLE TO DEFAULT; + +REVOKE ALL ON VARIABLE var1 FROM var_test_role; + +CREATE VARIABLE var AS integer; + +SELECT set_schema_variable('public.var', 1234); + +SELECT public.var; + +DO $$ +BEGIN + RAISE NOTICE 'public.var is = %', public.var; +END; +$$; + +CREATE OR REPLACE FUNCTION secure_var() +RETURNS int AS $$ + SELECT public.var; +$$ LANGUAGE sql SECURITY DEFINER; + +SELECT secure_var(); + +SET ROLE TO var_test_role; + +-- should to fail +SELECT public.var; + +-- should to work; +SELECT secure_var(); + +SET ROLE TO DEFAULT; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM generate_series(1,100) g(v) WHERE v = var; + +CREATE VIEW schema_var_view AS SELECT var; + +SELECT * FROM schema_var_view; + +\c - + +-- should to work still, but var will be empty +SELECT * FROM schema_var_view; + +LET var1 = pi(); + +SELECT var1; + +-- we can look on execution plan +EXPLAIN (VERBOSE, COSTS OFF) LET var1 = pi(); + +-- LET can be prepared +PREPARE var_pp(int, numeric) AS LET var1 = $1 + $2; + +EXECUTE var_pp(100, 1.23456); + +SELECT var1; + +CREATE VARIABLE var3 AS int; + +CREATE OR REPLACE FUNCTION inc(int) +RETURNS int AS $$ +BEGIN + LET public.var3 = COALESCE(public.var3 + $1, $1); + RETURN var3; +END; +$$ LANGUAGE plpgsql; + +SELECT inc(1); +SELECT inc(1); +SELECT inc(1); + +SELECT inc(1) FROM generate_series(1,10); + +SET ROLE TO var_test_role; + +-- should to fail +LET var3 = 0; + +SET ROLE TO DEFAULT; + +DROP VIEW schema_var_view; + +DROP VARIABLE var CASCADE; +DROP VARIABLE var1 CASCADE; +DROP VARIABLE var3 CASCADE; + +-- 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 (x int, y int, z numeric(10,2)); + +\d v1 +\d v2 + +-- explicit casting is necessary +LET v1 = (1,2,3.14)::sv_xyz; +LET v2 = (10,20,3.14*10)::v2; + +-- should to work too - there are prepared casts +LET v1 = (1,2,3.14)::v1; + +SELECT v1; +SELECT v2; +SELECT (v1).*; +SELECT (v2).*; + +SELECT get_schema_variable('v1', 'z', null::double precision); +SELECT get_schema_variable('v2', 'z', null::double precision); + +-- should to fail +SELECT get_schema_variable('v1', 'w', null::double precision); +SELECT get_schema_variable('v2', 'w', null::double precision); + +CREATE VARIABLE v3 AS integer; +SELECT get_schema_variable('v3', 'w', null::double precision); + +SELECT set_schema_variable('v1', 'z', 3.14 * 2); +SELECT v1; +SELECT set_schema_variable('v1', 'z', null::double precision); +SELECT v1; +SELECT set_schema_variable('v1', 'z', 3.3333::double precision); +SELECT set_schema_variable('v1', 'x', 10000.22); +SELECT v1; + +SELECT v1.x + v1.z; +SELECT v2.x + v2.z; + +-- access to composite fields should be safe too +-- should to fail +SET ROLE TO var_test_role; + +SELECT v2.x; + +SET ROLE TO DEFAULT; + + +DROP VARIABLE v1; +DROP VARIABLE v2; +DROP VARIABLE v3; + +DROP ROLE var_test_role; + +-- scalar variables should not be in conflict with qualified column +CREATE VARIABLE varx AS text; +SELECT varx.relname FROM pg_class varx WHERE varx.relname = 'pg_class'; + +-- should to fail +SELECT varx.xxx; + + +-- variables can be updated under RO transaction + +BEGIN; +SET TRANSACTION READ ONLY; +LET varx = 'hello'; +COMMIT; + +SELECT varx; + +DROP VARIABLE varx; + +CREATE VARIABLE v1 AS (a int, b numeric, c text); +LET v1 = (1, pi(), 'hello'); +SELECT v1; +LET v1.b = 10.2222; +SELECT v1; + +-- should to fail +LET v1.x = 10; + +DROP VARIABLE v1; + +-- arrays are supported +CREATE VARIABLE va1 AS numeric[]; +LET va1 = ARRAY[1.1,2.1]; +LET va1[1] = 10.1; +SELECT va1; + +CREATE VARIABLE va2 AS (a numeric, b numeric[]); +LET va2 = (10.1, ARRAY[0.0, 0.0]); +LET va2.a = 10.2; +SELECT va2; +LET va2.b[1] = 10.3; +SELECT va2; + +DROP VARIABLE va1; +DROP VARIABLE va2; + +-- default values +CREATE VARIABLE v1 AS numeric DEFAULT pi(); +LET v1 = v1 * 2; +SELECT v1; + +CREATE VARIABLE v2 AS (a numeric, b text DEFAULT 'hello'); +LET public.v2.a = pi(); +SELECT v2; + +DROP VARIABLE v1; +DROP VARIABLE v2; + diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 54850ee4d6..51e0a179b6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -431,6 +431,7 @@ CreateReplicationSlotCmd CreateRoleStmt CreateSchemaStmt CreateSchemaStmtContext +CreateSchemaVarStmt CreateSeqStmt CreateStatsStmt CreateStmt