From 4aed402a8f8813c792421aa7624d8367a2c92492 Mon Sep 17 00:00:00 2001 From: "okbob@github.com" Date: Tue, 5 Aug 2025 06:42:02 +0200 Subject: [PATCH 04/15] support of session variables for psql This patch enhancing psql to support session variables: * \dV[+] command * tab complete for CREATE, DROP, ALTER VARIABLE Note: tab complete for variable fencing is not supported yet --- doc/src/sgml/func/func-info.sgml | 13 ++++ doc/src/sgml/ref/psql-ref.sgml | 13 ++++ src/backend/catalog/namespace.c | 14 ++++ src/bin/psql/command.c | 3 + src/bin/psql/describe.c | 100 ++++++++++++++++++++++++++++- src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 45 +++++++++++-- src/include/catalog/pg_proc.dat | 3 + src/test/regress/expected/psql.out | 50 +++++++++++++++ src/test/regress/sql/psql.sql | 21 ++++++ 11 files changed, 259 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index a57f7665054..3aad9d0529f 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -1377,6 +1377,19 @@ SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); Is type (or domain) visible in search path? + + + + + pg_variable_is_visible + + pg_variable_is_visible ( variable oid ) + boolean + + + Is session variable visible in search path? + + diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 1a339600bc4..6bb3a9dad50 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2144,6 +2144,19 @@ SELECT $1 \parse stmt1 + + \dV[+] [ pattern ] + + + Lists session variables. + If pattern is + specified, only session variables whose names match the pattern are listed. + If the form \dV+ is used, additional information + about each variable is shown, like access privileges and description. + + + + \du[Sx+] [ pattern ] diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index ea49a0bc82d..78cabf964ef 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -5291,3 +5291,17 @@ pg_is_other_temp_schema(PG_FUNCTION_ARGS) PG_RETURN_BOOL(isOtherTempNamespace(oid)); } + +Datum +pg_variable_is_visible(PG_FUNCTION_ARGS) +{ + Oid oid = PG_GETARG_OID(0); + bool result; + bool is_missing = false; + + result = VariableIsVisibleExt(oid, &is_missing); + + if (is_missing) + PG_RETURN_NULL(); + PG_RETURN_BOOL(result); +} diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index cc602087db2..0ac7b9881cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1270,6 +1270,9 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) break; } break; + case 'V': /* Variables */ + success = listVariables(pattern, show_verbose); + break; case 'x': /* Extensions */ if (show_verbose) success = listExtensionContents(pattern); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 4aa793d7de7..1f5f7ced772 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1224,7 +1224,7 @@ listDefaultACLs(const char *pattern) "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " - " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" + " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s'" " WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' WHEN '%c' THEN '%s' END AS \"%s\",\n" " ", gettext_noop("Owner"), @@ -1241,6 +1241,8 @@ listDefaultACLs(const char *pattern) gettext_noop("schema"), DEFACLOBJ_LARGEOBJECT, gettext_noop("large object"), + DEFACLOBJ_VARIABLE, + gettext_noop("session variable"), gettext_noop("Type")); printACLColumn(&buf, "d.defaclacl"); @@ -5314,6 +5316,102 @@ error_return: return false; } +/* + * \dV + * + * listVariables() + */ +bool +listVariables(const char *pattern, bool verbose) +{ + PQExpBufferData buf; + PGresult *res; + printQueryOpt myopt = pset.popt; + static const bool translate_columns[] = {false, false, false, false, false, false, false}; + + if (pset.sversion < 180000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support session variables.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " v.varname as \"%s\",\n" + " pg_catalog.format_type(v.vartype, v.vartypmod) as \"%s\",\n" + " (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n" + " WHERE c.oid = v.varcollation AND bt.oid = v.vartype AND v.varcollation <> bt.typcollation) as \"%s\",\n" + " pg_catalog.pg_get_userbyid(v.varowner) as \"%s\"\n", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Type"), + gettext_noop("Collation"), + gettext_noop("Owner")); + + if (verbose) + { + appendPQExpBufferStr(&buf, ",\n "); + printACLColumn(&buf, "v.varacl"); + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(v.oid, 'pg_variable') AS \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_variable v" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = v.varnamespace"); + + appendPQExpBufferStr(&buf, "\nWHERE true\n"); + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "v.varname", NULL, + "pg_catalog.pg_variable_is_visible(v.oid)", + NULL, 3)) + return false; + + appendPQExpBufferStr(&buf, "ORDER BY 1,2;"); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + /* + * Most functions in this file are content to print an empty table when + * there are no matching objects. We intentionally deviate from that + * here, but only in !quiet mode, for historical reasons. + */ + if (PQntuples(res) == 0 && !pset.quiet) + { + if (pattern) + pg_log_error("Did not find any session variable named \"%s\".", + pattern); + else + pg_log_error("Did not find any session variables."); + } + else + { + myopt.nullPrint = NULL; + myopt.title = _("List of variables"); + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + } + + PQclear(res); + return true; +} /* * \dFp diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949..55ced4aab7b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -149,4 +149,7 @@ extern bool listOpFamilyFunctions(const char *access_method_pattern, /* \dl or \lo_list */ extern bool listLargeObjects(bool verbose); +/* \dV */ +extern bool listVariables(const char *pattern, bool varbose); + #endif /* DESCRIBE_H */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index ed00c36695e..aa91a7ce10f 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -266,6 +266,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dT[Sx+] [PATTERN] list data types\n"); HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); + HELP0(" \\dV[x+] [PATTERN] list session variables\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 6b20a4404b2..928b9dda714 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -978,6 +978,13 @@ static const SchemaQuery Query_for_trigger_of_table = { .refnamespace = "c1.relnamespace", }; +static const SchemaQuery Query_for_list_of_variables = { + .min_server_version = 180000, + .catname = "pg_catalog.pg_variable v", + .viscondition = "pg_catalog.pg_variable_is_visible(v.oid)", + .namespace = "v.varnamespace", + .result = "v.varname", +}; /* * Queries to get lists of names of various kinds of things, possibly @@ -1342,6 +1349,7 @@ static const pgsql_thing_t words_after_create[] = { * TABLE ... */ {"USER", Query_for_list_of_roles, NULL, NULL, Keywords_for_user_thing}, {"USER MAPPING FOR", NULL, NULL, NULL}, + {"VARIABLE", NULL, NULL, &Query_for_list_of_variables}, {"VIEW", NULL, NULL, &Query_for_list_of_views}, {NULL} /* end of list */ }; @@ -1907,7 +1915,7 @@ psql_completion(const char *text, int start, int end) "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", - "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", + "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", "\\dV", "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\endpipeline", "\\errverbose", "\\ev", "\\f", "\\flush", "\\flushrequest", @@ -2621,6 +2629,9 @@ match_previous_words(int pattern_id, "ALL"); else if (Matches("ALTER", "SYSTEM", "SET", MatchAny)) COMPLETE_WITH("TO"); + /* ALTER VARIABLE */ + else if (Matches("ALTER", "VARIABLE", MatchAny)) + COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA"); /* ALTER VIEW */ else if (Matches("ALTER", "VIEW", MatchAny)) COMPLETE_WITH("ALTER COLUMN", "OWNER TO", "RENAME", "RESET", "SET"); @@ -3227,7 +3238,7 @@ match_previous_words(int pattern_id, "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", - "TRIGGER", "TYPE", "VIEW"); + "TRIGGER", "TYPE", "VARIABLE", "VIEW"); else if (Matches("COMMENT", "ON", "ACCESS", "METHOD")) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (Matches("COMMENT", "ON", "CONSTRAINT")) @@ -4041,6 +4052,13 @@ match_previous_words(int pattern_id, else if (TailMatches("=", MatchAnyExcept("*)"))) COMPLETE_WITH(",", ")"); } +/* CREATE VARIABLE --- is allowed inside CREATE SCHEMA, so use TailMatches */ + /* Complete CREATE VARIABLE with AS */ + else if (TailMatches("CREATE", "VARIABLE", MatchAny)) + COMPLETE_WITH("AS"); + else if (TailMatches("VARIABLE", MatchAny, "AS")) + /* Complete CREATE VARIABLE with AS types */ + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* CREATE VIEW --- is allowed inside CREATE SCHEMA, so use TailMatches */ /* Complete CREATE [ OR REPLACE ] VIEW with AS or WITH */ @@ -4318,6 +4336,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "TRANSFORM", "FOR", MatchAny, "LANGUAGE", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP VARIABLE */ + else if (Matches("DROP", "VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); + else if (Matches("DROP", "VARIABLE", MatchAny)) + COMPLETE_WITH("CASCADE", "RESTRICT"); + /* EXECUTE */ else if (Matches("EXECUTE")) COMPLETE_WITH_QUERY(Query_for_list_of_prepared_statements); @@ -4519,7 +4543,9 @@ match_previous_words(int pattern_id, * objects supported. */ if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) - COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS"); + COMPLETE_WITH("TABLES", "SEQUENCES", "FUNCTIONS", "PROCEDURES", + "ROUTINES", "TYPES", "SCHEMAS", "LARGE OBJECTS", + "VARIABLES"); else COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_grantables, "ALL FUNCTIONS IN SCHEMA", @@ -4527,6 +4553,7 @@ match_previous_words(int pattern_id, "ALL ROUTINES IN SCHEMA", "ALL SEQUENCES IN SCHEMA", "ALL TABLES IN SCHEMA", + "ALL VARIABLES IN SCHEMA", "DATABASE", "DOMAIN", "FOREIGN DATA WRAPPER", @@ -4541,7 +4568,8 @@ match_previous_words(int pattern_id, "SEQUENCE", "TABLE", "TABLESPACE", - "TYPE"); + "TYPE", + "VARIABLE"); } else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "ALL")) @@ -4549,7 +4577,8 @@ match_previous_words(int pattern_id, "PROCEDURES IN SCHEMA", "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", - "TABLES IN SCHEMA"); + "TABLES IN SCHEMA", + "VARIABLES IN SCHEMA"); else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) COMPLETE_WITH("DATA WRAPPER", "SERVER"); @@ -4585,6 +4614,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); else if (TailMatches("TYPE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); + else if (TailMatches("VARIABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny)) COMPLETE_WITH("TO"); else @@ -4911,7 +4942,7 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", "LET"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -5400,6 +5431,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\dv*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); + else if (TailMatchesCS("\\dV*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_variables); else if (TailMatchesCS("\\dx*")) COMPLETE_WITH_QUERY(Query_for_list_of_extensions); else if (TailMatchesCS("\\dX*")) diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f6dd9dadec5..593bd95eb23 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6709,6 +6709,9 @@ proname => 'pg_collation_is_visible', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_collation_is_visible' }, +{ oid => '9999', descr => 'is session variable visible in search path?', + proname => 'pg_variable_is_visible', procost => '10', provolatile => 's', + prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_variable_is_visible' }, { oid => '2854', descr => 'get OID of current session\'s temp schema, if any', proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r', diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a79325e8a2f..f2e506796db 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -6025,6 +6025,30 @@ COMMIT; # final ON_ERROR_ROLLBACK: off DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+-------------------+------------- + public | var1 | character varying | C | regress_variable_owner | | +(1 row) + +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 + List of variables + Schema | Name | Type | Collation | Owner | Access privileges | Description +--------+------+-------------------+-----------+------------------------+--------------------------------------------------+------------------ + public | var1 | character varying | C | regress_variable_owner | regress_variable_owner=rw/regress_variable_owner+| some description + | | | | | =r/regress_variable_owner | +(1 row) + +DROP VARIABLE var1; +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; -- check describing invalid multipart names \dA regression.heap improper qualified name (too many dotted names): regression.heap @@ -6246,6 +6270,12 @@ cross-database references are not implemented: nonesuch.public.func_deps_stat improper qualified name (too many dotted names): regression.myevt \dy nonesuch.myevt improper qualified name (too many dotted names): nonesuch.myevt +\dV host.regression.public.var +improper qualified name (too many dotted names): host.regression.public.var +\dV regression|mydb.public.var +cross-database references are not implemented: regression|mydb.public.var +\dV nonesuch.public.var +cross-database references are not implemented: nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" List of access methods @@ -6480,6 +6510,12 @@ List of schemas ------+-------+-------+---------+----------+------ (0 rows) +\dV "no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" improper qualified name (too many dotted names): "no.such.schema"."no.such.access.method" @@ -6649,6 +6685,12 @@ improper qualified name (too many dotted names): "no.such.schema"."no.such.insta \dy "no.such.schema"."no.such.event.trigger" improper qualified name (too many dotted names): "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" List of tables @@ -6782,6 +6824,12 @@ List of text search templates --------+------+------------+-----------+--------------+----- (0 rows) +\dV regression."no.such.schema"."no.such.variable" + List of variables + Schema | Name | Type | Collation | Owner +--------+------+------+-----------+------- +(0 rows) + -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.table.relation" @@ -6829,6 +6877,8 @@ cross-database references are not implemented: "no.such.database"."no.such.schem cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" +cross-database references are not implemented: "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; CREATE ROLE regress_du_role1; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index f064e4f5456..8f6108e44de 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1645,6 +1645,19 @@ COMMIT; DROP TABLE bla; DROP FUNCTION psql_error; +-- session variable test +CREATE ROLE regress_variable_owner; +SET ROLE TO regress_variable_owner; +CREATE VARIABLE var1 AS varchar COLLATE "C"; +\dV+ var1 +GRANT SELECT ON VARIABLE var1 TO PUBLIC; +COMMENT ON VARIABLE var1 IS 'some description'; +\dV+ var1 +DROP VARIABLE var1; + +SET ROLE TO DEFAULT; +DROP ROLE regress_variable_owner; + -- check describing invalid multipart names \dA regression.heap \dA nonesuch.heap @@ -1756,6 +1769,9 @@ DROP FUNCTION psql_error; \dX nonesuch.public.func_deps_stat \dy regression.myevt \dy nonesuch.myevt +\dV host.regression.public.var +\dV regression|mydb.public.var +\dV nonesuch.public.var -- check that dots within quoted name segments are not counted \dA "no.such.access.method" @@ -1797,6 +1813,8 @@ DROP FUNCTION psql_error; \dx "no.such.installed.extension" \dX "no.such.extended.statistics" \dy "no.such.event.trigger" +\dV "no.such.variable" + -- again, but with dotted schema qualifications. \dA "no.such.schema"."no.such.access.method" @@ -1837,6 +1855,7 @@ DROP FUNCTION psql_error; \dx "no.such.schema"."no.such.installed.extension" \dX "no.such.schema"."no.such.extended.statistics" \dy "no.such.schema"."no.such.event.trigger" +\dV "no.such.schema"."no.such.variable" -- again, but with current database and dotted schema qualifications. \dt regression."no.such.schema"."no.such.table.relation" @@ -1861,6 +1880,7 @@ DROP FUNCTION psql_error; \dP regression."no.such.schema"."no.such.partitioned.relation" \dT regression."no.such.schema"."no.such.data.type" \dX regression."no.such.schema"."no.such.extended.statistics" +\dV regression."no.such.schema"."no.such.variable" -- again, but with dotted database and dotted schema qualifications. \dt "no.such.database"."no.such.schema"."no.such.table.relation" @@ -1886,6 +1906,7 @@ DROP FUNCTION psql_error; \dP "no.such.database"."no.such.schema"."no.such.partitioned.relation" \dT "no.such.database"."no.such.schema"."no.such.data.type" \dX "no.such.database"."no.such.schema"."no.such.extended.statistics" +\dV "no.such.database"."no.such.schema"."no.such.variable" -- check \drg and \du CREATE ROLE regress_du_role0; -- 2.51.0