From ad81b71fe67174aebd7f6c02a82aa0bbbbab88ff Mon Sep 17 00:00:00 2001
From: Sadeq Dousti <3616518+msdousti@users.noreply.github.com>
Date: Sat, 22 Feb 2025 23:16:44 +0100
Subject: [PATCH v2] psql \dh: list high-level tables and indexes
---
doc/src/sgml/ref/psql-ref.sgml | 22 ++++
src/bin/psql/command.c | 18 +++
src/bin/psql/describe.c | 196 +++++++++++++++++++++++++++++
src/bin/psql/describe.h | 3 +
src/bin/psql/help.c | 1 +
src/bin/psql/tab-complete.in.c | 43 ++++++-
src/test/regress/expected/psql.out | 59 +++++++++
src/test/regress/sql/psql.sql | 13 ++
8 files changed, 354 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 3edbd65e46c..17b55b47d50 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1871,6 +1871,28 @@ SELECT $1 \parse stmt1
+
+ \dh[it+] [ pattern ]
+
+
+ Lists high-level (root) relations. This means relations that are
+ either not partitioned, or partitioned and have no parent.
+ If pattern
+ is specified, only entries whose name matches the pattern are listed.
+ The modifiers t (tables) and i
+ (indexes) can be appended to the command, filtering the kind of
+ relations to list. By default, high-level tables and indexes are
+ listed.
+
+
+
+ If + is appended to the command, the size of the
+ relation (when it's not partitioned), or the sum of the sizes of
+ the table's partitions (when the relation is partitioned) is also
+ displayed, along with the associated description.
+
+
+
\dl[x+]
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0f27bf7a91f..9fff1864010 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1117,6 +1117,24 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
/* no longer distinct from \du */
success = describeRoles(pattern, show_verbose, show_system);
break;
+ case 'h':
+ /* high-level (i.e., root) tables and indexes */
+ {
+ switch (cmd[2])
+ {
+ case '\0':
+ case '+':
+ case 't':
+ case 'i':
+ case 'x':
+ success = listRootTables(&cmd[2], pattern, show_verbose);
+ break;
+ default:
+ status = PSQL_CMD_UNKNOWN;
+ break;
+ }
+ }
+ break;
case 'l':
success = listLargeObjects(show_verbose);
break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..33270650511 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4247,6 +4247,202 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
return true;
}
+/*
+ * \dh
+ * Takes an optional regexp to select particular relations
+ *
+ * As with \d, you can specify the kinds of relations you want:
+ *
+ * t for tables
+ * i for indexes
+ *
+ * and you can mix and match them in any order.
+ */
+bool
+listRootTables(const char *reltypes, const char *pattern, bool verbose)
+{
+ bool showTables = strchr(reltypes, 't') != NULL;
+ bool showIndexes = strchr(reltypes, 'i') != NULL;
+ PQExpBufferData buf;
+ PQExpBufferData title;
+ PGresult *res;
+ printQueryOpt myopt = pset.popt;
+ bool translate_columns[] = {false, false, false, false, false, false, false, false};
+ const char *tabletitle;
+ bool mixed_output = false;
+
+ /*
+ * Note: Declarative table partitioning is only supported as of Pg 10.0.
+ */
+ if (pset.sversion < 100000)
+ {
+ char sverbuf[32];
+
+ pg_log_error("The server (version %s) does not support declarative table partitioning.",
+ formatPGVersionNumber(pset.sversion, false,
+ sverbuf, sizeof(sverbuf)));
+ return true;
+ }
+
+ /* If no relation kind was selected, show them all */
+ if (!showTables && !showIndexes)
+ showTables = showIndexes = true;
+
+ if (showIndexes && !showTables)
+ tabletitle = _("List of root indexes"); /* \dhi */
+ else if (showTables && !showIndexes)
+ tabletitle = _("List of root tables"); /* \dht */
+ else
+ {
+ /* show all kinds */
+ tabletitle = _("List of root relations");
+ mixed_output = true;
+ }
+
+ initPQExpBuffer(&buf);
+
+ printfPQExpBuffer(&buf,
+ "SELECT n.nspname as \"%s\",\n"
+ " c.relname as \"%s\",\n"
+ " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"",
+ gettext_noop("Schema"),
+ gettext_noop("Name"),
+ gettext_noop("Owner"));
+
+ if (mixed_output)
+ {
+ appendPQExpBuffer(&buf,
+ ",\n CASE c.relkind"
+ " WHEN " CppAsString2(RELKIND_RELATION) " THEN '%s'"
+ " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'"
+ " WHEN " CppAsString2(RELKIND_INDEX) " THEN '%s'"
+ " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'"
+ " END as \"%s\"",
+ gettext_noop("table"),
+ gettext_noop("partitioned table"),
+ gettext_noop("index"),
+ gettext_noop("partitioned index"),
+ gettext_noop("Type"));
+
+ translate_columns[3] = true;
+ }
+
+ if (showIndexes)
+ appendPQExpBuffer(&buf,
+ ",\n c2.oid::pg_catalog.regclass as \"%s\"",
+ gettext_noop("Table"));
+
+ if (verbose)
+ {
+ /*
+ * Table access methods were introduced in v12, and can be set on
+ * partitioned tables since v17.
+ */
+ appendPQExpBuffer(&buf, ",\n am.amname as \"%s\"",
+ gettext_noop("Access method"));
+
+ /* Sizes of all partitions are considered in this case. */
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(GREATEST(pg_catalog.pg_table_size(c.oid), s.tps)) as \"%s\"",
+ gettext_noop("Total size"));
+
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"",
+ gettext_noop("Description"));
+ }
+
+ appendPQExpBufferStr(&buf,
+ "\nFROM pg_catalog.pg_class c"
+ "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace");
+
+ if (showIndexes)
+ appendPQExpBufferStr(&buf,
+ "\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid"
+ "\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid");
+
+ if (verbose)
+ {
+ appendPQExpBufferStr(&buf,
+ "\n LEFT JOIN pg_catalog.pg_am am ON c.relam = am.oid");
+
+ if (pset.sversion < 120000)
+ {
+ appendPQExpBufferStr(&buf,
+ ",\n LATERAL (WITH RECURSIVE d\n"
+ " AS (SELECT inhrelid AS oid, 1 AS level\n"
+ " FROM pg_catalog.pg_inherits\n"
+ " WHERE inhparent = c.oid\n"
+ " UNION ALL\n"
+ " SELECT inhrelid, level + 1\n"
+ " FROM pg_catalog.pg_inherits i\n"
+ " JOIN d ON i.inhparent = d.oid)\n"
+ " SELECT sum(pg_catalog.pg_table_size(d.oid)) AS tps\n"
+ " FROM d) s");
+ }
+ else
+ {
+ /* PostgreSQL 12 has pg_partition_tree function */
+ appendPQExpBufferStr(&buf,
+ ",\n LATERAL (SELECT sum(pg_catalog.pg_table_size(ppt.relid)) AS tps"
+ "\n FROM pg_catalog.pg_partition_tree(c.oid) ppt) s");
+ }
+ }
+
+ appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN (");
+ if (showTables)
+ {
+ appendPQExpBufferStr(&buf, CppAsString2(RELKIND_RELATION) ",");
+ appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PARTITIONED_TABLE) ",");
+ }
+ if (showIndexes)
+ {
+ appendPQExpBufferStr(&buf, CppAsString2(RELKIND_INDEX) ",");
+ appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PARTITIONED_INDEX) ",");
+ }
+ appendPQExpBufferStr(&buf, "''"); /* dummy */
+ appendPQExpBufferStr(&buf, ")\n");
+
+ appendPQExpBufferStr(&buf, " AND NOT c.relispartition\n");
+
+ if (!pattern)
+ appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n"
+ " AND n.nspname !~ '^pg_toast'\n"
+ " AND n.nspname <> 'information_schema'\n");
+
+ if (!validateSQLNamePattern(&buf, pattern, true, false,
+ "n.nspname", "c.relname", NULL,
+ "pg_catalog.pg_table_is_visible(c.oid)",
+ NULL, 3))
+ {
+ termPQExpBuffer(&buf);
+ return false;
+ }
+
+ appendPQExpBuffer(&buf, "ORDER BY \"Schema\", %s\"Name\";",
+ mixed_output ? "\"Type\" DESC, " : "");
+
+ res = PSQLexec(buf.data);
+ termPQExpBuffer(&buf);
+ if (!res)
+ return false;
+
+ initPQExpBuffer(&title);
+ appendPQExpBufferStr(&title, tabletitle);
+
+ myopt.title = title.data;
+ myopt.translate_header = true;
+ myopt.translate_columns = translate_columns;
+ myopt.n_translate_columns = lengthof(translate_columns);
+
+ printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+ termPQExpBuffer(&title);
+
+ PQclear(res);
+ return true;
+}
+
+
/*
* \dP
* Takes an optional regexp to select particular relations
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 18ecaa60949..6abd7ba31b6 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -70,6 +70,9 @@ extern bool listAllDbs(const char *pattern, bool verbose);
/* \dt, \di, \ds, \dS, etc. */
extern bool listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSystem);
+/* \dh */
+extern bool listRootTables(const char *reltypes, const char *pattern, bool verbose);
+
/* \dP */
extern bool listPartitionedTables(const char *reltypes, const char *pattern, bool verbose);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 714b8619233..3f75be4644f 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -254,6 +254,7 @@ slashUsage(unsigned short int pager)
HELP0(" \\dFp[x+] [PATTERN] list text search parsers\n");
HELP0(" \\dFt[x+] [PATTERN] list text search templates\n");
HELP0(" \\dg[Sx+] [PATTERN] list roles\n");
+ HELP0(" \\dh[tix+] [PATTERN] list high-level (root) relations\n");
HELP0(" \\di[Sx+] [PATTERN] list indexes\n");
HELP0(" \\dl[x+] list large objects, same as \\lo_list\n");
HELP0(" \\dL[Sx+] [PATTERN] list procedural languages\n");
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8432be641ac..2e0ff93b69a 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -691,6 +691,16 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = {
.result = "c.relname",
};
+static const SchemaQuery Query_for_list_of_root_tables = {
+ .catname = "pg_catalog.pg_class c",
+ .selcondition = "c.relispartition = false AND "
+ "c.relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_PARTITIONED_TABLE) ")",
+ .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+ .namespace = "c.relnamespace",
+ .result = "c.relname",
+};
+
static const SchemaQuery Query_for_list_of_tables_for_constraint = {
.catname = "pg_catalog.pg_class c, pg_catalog.pg_constraint con",
.selcondition = "c.oid=con.conrelid and c.relkind IN ("
@@ -795,6 +805,16 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
.result = "c.relname",
};
+static const SchemaQuery Query_for_list_of_root_indexes = {
+ .catname = "pg_catalog.pg_class c",
+ .selcondition = "c.relispartition = false AND "
+ "c.relkind IN (" CppAsString2(RELKIND_INDEX) ", "
+ CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
+ .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+ .namespace = "c.relnamespace",
+ .result = "c.relname",
+};
+
/* All relations */
static const SchemaQuery Query_for_list_of_relations = {
@@ -814,6 +834,19 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = {
.result = "c.relname",
};
+/* root relations */
+static const SchemaQuery Query_for_list_of_root_relations = {
+ .catname = "pg_catalog.pg_class c",
+ .selcondition = "c.relispartition = false AND "
+ "c.relkind IN (" CppAsString2(RELKIND_RELATION) ", "
+ CppAsString2(RELKIND_PARTITIONED_TABLE) ", "
+ CppAsString2(RELKIND_INDEX) ", "
+ CppAsString2(RELKIND_PARTITIONED_INDEX) ")",
+ .viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+ .namespace = "c.relnamespace",
+ .result = "c.relname",
+};
+
static const SchemaQuery Query_for_list_of_operator_families = {
.catname = "pg_catalog.pg_opfamily c",
.viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)",
@@ -1880,7 +1913,8 @@ psql_completion(const char *text, int start, int end)
"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
- "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
+ "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\dh", "\\dhi", "\\dht",
+ "\\di", "\\dl", "\\dL",
"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
"\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds",
"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy",
@@ -5269,8 +5303,15 @@ match_previous_words(int pattern_id,
else if (TailMatchesCS("\\dFt*"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ts_templates);
/* must be at end of \dF alternatives: */
+
else if (TailMatchesCS("\\dF*"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ts_configurations);
+ else if (TailMatchesCS("\\dhi*"))
+ COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_indexes);
+ else if (TailMatchesCS("\\dht*"))
+ COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_tables);
+ else if (TailMatchesCS("\\dh*"))
+ COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_relations);
else if (TailMatchesCS("\\di*"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes);
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 6543e90de75..7ba2fff3919 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5017,6 +5017,33 @@ create index testpart_orange_index on testpart_orange(logdate);
testpart | testpart_apple_index | regress_partitioning_role | | testpart_apple
(1 row)
+-- only root relations should be displayed
+\dh test*apple*
+ List of root relations
+ Schema | Name | Owner | Type | Table
+----------+-----------------------+---------------------------+-------------------+-----------------
+ testpart | testtable_apple | regress_partitioning_role | table |
+ testpart | testpart_apple | regress_partitioning_role | partitioned table |
+ testpart | testpart_apple_index | regress_partitioning_role | partitioned index | testpart_apple
+ testpart | testtable_apple_index | regress_partitioning_role | index | testtable_apple
+(4 rows)
+
+\dht test*apple*
+ List of root tables
+ Schema | Name | Owner
+----------+-----------------+---------------------------
+ testpart | testpart_apple | regress_partitioning_role
+ testpart | testtable_apple | regress_partitioning_role
+(2 rows)
+
+\dhi test*apple*
+ List of root indexes
+ Schema | Name | Owner | Table
+----------+-----------------------+---------------------------+-----------------
+ testpart | testpart_apple_index | regress_partitioning_role | testpart_apple
+ testpart | testtable_apple_index | regress_partitioning_role | testtable_apple
+(2 rows)
+
drop table testtable_apple;
drop table testtable_orange;
drop table testpart_apple;
@@ -5038,6 +5065,7 @@ create table child_30_35 partition of child_30_40
create table child_35_40 partition of child_30_40
for values from (35) to (40);
insert into parent_tab values (generate_series(30,39));
+-- only partition related object should be displayed
\dPt
List of partitioned tables
Schema | Name | Owner
@@ -5106,6 +5134,37 @@ insert into parent_tab values (generate_series(30,39));
testpart | child_30_40_id_idx | regress_partitioning_role | partitioned index | parent_index | child_30_40
(4 rows)
+-- only root relations should be displayed
+\dht
+ List of root tables
+ Schema | Name | Owner
+----------+------------+---------------------------
+ testpart | parent_tab | regress_partitioning_role
+(1 row)
+
+\dhi
+ List of root indexes
+ Schema | Name | Owner | Table
+----------+--------------+---------------------------+------------
+ testpart | parent_index | regress_partitioning_role | parent_tab
+(1 row)
+
+\dh testpart.*
+ List of root relations
+ Schema | Name | Owner | Type | Table
+----------+--------------+---------------------------+-------------------+------------
+ testpart | parent_tab | regress_partitioning_role | partitioned table |
+ testpart | parent_index | regress_partitioning_role | partitioned index | parent_tab
+(2 rows)
+
+\dh
+ List of root relations
+ Schema | Name | Owner | Type | Table
+----------+--------------+---------------------------+-------------------+------------
+ testpart | parent_tab | regress_partitioning_role | partitioned table |
+ testpart | parent_index | regress_partitioning_role | partitioned index | parent_tab
+(2 rows)
+
drop table parent_tab cascade;
drop schema testpart;
set search_path to default;
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 97d1be3aac3..5606f70a106 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1257,6 +1257,11 @@ create index testpart_orange_index on testpart_orange(logdate);
\dPt test*apple*
\dPi test*apple*
+-- only root relations should be displayed
+\dh test*apple*
+\dht test*apple*
+\dhi test*apple*
+
drop table testtable_apple;
drop table testtable_orange;
drop table testpart_apple;
@@ -1280,6 +1285,7 @@ create table child_35_40 partition of child_30_40
for values from (35) to (40);
insert into parent_tab values (generate_series(30,39));
+-- only partition related object should be displayed
\dPt
\dPi
@@ -1291,6 +1297,13 @@ insert into parent_tab values (generate_series(30,39));
\dPn
\dPn testpart.*
+-- only root relations should be displayed
+\dht
+\dhi
+
+\dh testpart.*
+\dh
+
drop table parent_tab cascade;
drop schema testpart;
--
2.43.0