[PATCH] psql: add \dcs to list all constraints
Hi hackers,
Until PostgreSQL 17, if you wanted to inspect constraints in a database,
you had to either:
- use the "\d" command to check constraints per table, or
- query "pg_constraint" and "pg_attribute" directly.
However, starting from PG18, thanks to Álvaro's work [1]Changes to NOT NULL in Postgres 18 https://www.enterprisedb.com/blog/changes-not-null-postgres-18, NOT NULL
constraints are now included in "pg_constraint". This means that by
querying "pg_constraint", we can now obtain all kinds of constraints [2]5.5. Constraints https://www.postgresql.org/docs/current/ddl-constraints.html[3]constraint trigger
directly.
Building on that improvement, I would like to propose a new psql
meta-command to **list all constraints** in the database.
## Motivation
This command would help DBAs and users to easily understand:
- which tables have constraints
- how many constraints exist in the database overall
- whether all constraints have been properly created after a migration,
etc.
In other words, it would serve as a convenient tool for quickly
validating schema integrity.
Given that psql already has a wide variety of meta-commands — from
frequently used to rarely used ones — I believe adding this one would
not cause any issues.
## Usage examples
-- Show all constraints
\dcs+ con_*
List of constraints
Schema | Name | Definition
| Table
--------+----------------------------+----------------------
-----------------------------------+-------
public | con_c_pkey | PRIMARY KEY (primary_col)
| con_c
public | con_c_primary_col_not_null | NOT NULL primary_col
| con_c
public | con_p_check_col_check | CHECK ((check_col >= 0))
| con_p
public | con_p_exclusion | EXCLUDE USING btree (exclusion_col
WITH =) | con_p
public | con_p_foreign_col_fkey | FOREIGN KEY (foreign_col) REFERENCES
con_c(primary_col) | con_p
public | con_p_notnull_col_not_null | NOT NULL notnull_col
| con_p
public | con_p_pkey | PRIMARY KEY (primary_col)
| con_p
public | con_p_primary_col_not_null | NOT NULL primary_col
| con_p
public | con_p_trigger | TRIGGER
| con_p
public | con_p_unique_col_key | UNIQUE (unique_col)
| con_p
(10 rows)
-- Show only NOT NULL constraints (added "n" for filter)
\dcsn+ con_*
List of constraints
Schema | Name | Definition | Table
--------+----------------------------+----------------------+-------
public | con_c_primary_col_not_null | NOT NULL primary_col | con_c
public | con_p_notnull_col_not_null | NOT NULL notnull_col | con_p
public | con_p_primary_col_not_null | NOT NULL primary_col | con_p
(3 rows)
## About the patch
The patch includes:
- \dcs meta-command
- Tab completion support
- Regression tests
- Documentation
and applies cleanly to the master branch.
## Discussion point: meta-command naming
I'd like to ask for opinions (and votes) on the command name.
Here are some candidates. The one with the most votes will be adopted
as the final name.
\dcs
-> uses the first letters of the two syllables in "con-straint"
\dco
-> short form using the first two letters of "constraint"
\G
-> an idea based on the synonym "guarantee," which semantically fits
the concept
Please find the attached file.
Feedback and suggestions are very welcome.
[1]: Changes to NOT NULL in Postgres 18 https://www.enterprisedb.com/blog/changes-not-null-postgres-18
https://www.enterprisedb.com/blog/changes-not-null-postgres-18
[2]: 5.5. Constraints https://www.postgresql.org/docs/current/ddl-constraints.html
https://www.postgresql.org/docs/current/ddl-constraints.html
[3]: constraint trigger
https://www.postgresql.org/docs/current/sql-createtrigger.html#id-1.9.3.93.6
Thanks,
Tatsuro Yamada
Attachments:
0001-Add-list-constraints-meta-command-dcs-on-psql.patchapplication/octet-stream; name=0001-Add-list-constraints-meta-command-dcs-on-psql.patchDownload
From 6843227173a153f6a95e6f1f114cc65e308e8ab2 Mon Sep 17 00:00:00 2001
From: Tatsuro Yamada <yamatattsu@gmail.com>
Date: Sat, 25 Oct 2025 11:11:40 +0900
Subject: [PATCH] Add list constraints meta-command \dcs on psql
\dco shows all kind of constraints by using pg_constraint.
You can filter constraints by appending c/f/n/p/u/e to \dcs.
For example \dcsc will show only check constraints.
This patch also includes:
- document
- regression test
- tab completion
---
doc/src/sgml/ref/psql-ref.sgml | 20 ++-
src/bin/psql/command.c | 19 ++
src/bin/psql/describe.c | 117 +++++++++++++
src/bin/psql/describe.h | 4 +
src/bin/psql/help.c | 2 +
src/bin/psql/tab-complete.in.c | 4 +-
src/test/regress/expected/psql.out | 271 +++++++++++++++++++++++++++++
src/test/regress/sql/psql.sql | 64 +++++++
8 files changed, 499 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 1a339600bc4..fd3bc6c7956 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1548,6 +1548,25 @@ SELECT $1 \parse stmt1
</listitem>
</varlistentry>
+ <varlistentry id="app-psql-meta-command-dcs">
+ <term><literal>\dcs[cfnptue][Sx+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+ <listitem>
+ <para>
+ Lists constraints.
+ If <replaceable class="parameter">pattern</replaceable>
+ is specified, only entries whose name matches the pattern are listed.
+ The modifiers <literal>c</literal> (check), <literal>f</literal> (foreign key),
+ <literal>n</literal> (not-null), <literal>p</literal> (primary key),
+ <literal>t</literal> (trigger), <literal>u</literal> (unique),
+ <literal>e</literal> (exclusion) can be appended to the command,
+ filtering the kind of constraints to list.
+ By default, only user-created constraints are shown; supply the
+ <literal>S</literal> modifier to include system objects.
+ If <literal>+</literal> is appended to the command name, each object
+ is listed with its associated description.
+ </para>
+ </listitem>
+ </varlistentry>
<varlistentry id="app-psql-meta-command-dc-uc">
<term><literal>\dC[x+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
@@ -1566,7 +1585,6 @@ SELECT $1 \parse stmt1
</listitem>
</varlistentry>
-
<varlistentry id="app-psql-meta-command-dd-lc">
<term><literal>\dd[Sx] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
<listitem>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index cc602087db2..05ce809090e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1103,6 +1103,25 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
success = describeConfigurationParameters(pattern,
show_verbose,
show_system);
+ else if (strncmp(cmd, "dcs", 3) == 0) /* Constraint */
+ switch (cmd[3])
+ {
+ case '\0':
+ case '+':
+ case 'S':
+ case 'c':
+ case 'f':
+ case 'n':
+ case 'p':
+ case 't':
+ case 'u':
+ case 'e':
+ success = listConstraints(&cmd[3], pattern, show_verbose, show_system);
+ break;
+ default:
+ status = PSQL_CMD_UNKNOWN;
+ break;
+ }
else
success = listConversions(pattern,
show_verbose,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..6da6a0e4055 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4980,6 +4980,123 @@ listExtendedStats(const char *pattern)
return true;
}
+/*
+ * \dcs
+ * Describes constraints
+ *
+ * As with \d, you can specify the kinds of constraints you want:
+ *
+ * c for check
+ * f for foreign key
+ * n for not null
+ * p for primary key
+ * t for trigger
+ * u for unique
+ * e for exclusion
+ *
+ * and you can mix and match these in any order.
+ */
+bool
+listConstraints(const char *contypes, const char *pattern, bool verbose, bool showSystem)
+{
+ bool showCheck = strchr(contypes, CONSTRAINT_CHECK) != NULL;
+ bool showForeign = strchr(contypes, CONSTRAINT_FOREIGN) != NULL;
+ bool showNotnull = strchr(contypes, CONSTRAINT_NOTNULL) != NULL;
+ bool showPrimary = strchr(contypes, CONSTRAINT_PRIMARY) != NULL;
+ bool showTrigger = strchr(contypes, CONSTRAINT_TRIGGER) != NULL;
+ bool showUnique = strchr(contypes, CONSTRAINT_UNIQUE) != NULL;
+ bool showExclusion = strchr(contypes, 'e') != NULL;
+ bool showAllkinds = false;
+ PQExpBufferData buf;
+ PGresult *res;
+ printQueryOpt myopt = pset.popt;
+
+ if (pset.sversion < 180000)
+ {
+ char sverbuf[32];
+
+ pg_log_error("The server (version %s) does not support this meta-command on psql.",
+ formatPGVersionNumber(pset.sversion, false,
+ sverbuf, sizeof(sverbuf)));
+ return true;
+ }
+
+ /* If contypes were not selected, show them all */
+ if (!(showCheck || showForeign || showNotnull || showPrimary || showTrigger || showUnique || showExclusion))
+ showAllkinds = true;
+
+ initPQExpBuffer(&buf);
+ printfPQExpBuffer(&buf,
+ "SELECT n.nspname AS \"%s\", \n"
+ " cst.conname AS \"%s\" ",
+ gettext_noop("Schema"),
+ gettext_noop("Name")
+ );
+
+ if (verbose)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_get_constraintdef(cst.oid) AS \"%s\", \n"
+ " cst.conrelid::pg_catalog.regclass AS \"%s\" ",
+ gettext_noop("Definition"),
+ gettext_noop("Table")
+ );
+
+ appendPQExpBufferStr(&buf,
+ "\nFROM pg_catalog.pg_constraint cst \n"
+ " JOIN pg_catalog.pg_namespace n ON n.oid = cst.connamespace \n"
+ );
+
+ if (!showSystem && !pattern)
+ appendPQExpBufferStr(&buf,
+ "WHERE n.nspname <> 'pg_catalog' \n"
+ " AND n.nspname <> 'information_schema' \n");
+
+ processSQLNamePattern(pset.db, &buf,
+ pattern,
+ !showSystem && !pattern, false,
+ "n.nspname", "cst.conname",
+ NULL, "pg_catalog.pg_table_is_visible(cst.conrelid)",
+ NULL, NULL);
+
+ if (!showAllkinds)
+ {
+ appendPQExpBufferStr(&buf, " AND cst.contype in (");
+
+ if (showCheck)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_CHECK) ",");
+ if (showForeign)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_FOREIGN) ",");
+ if (showNotnull)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_NOTNULL) ",");
+ if (showPrimary)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_PRIMARY) ",");
+ if (showTrigger)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_TRIGGER) ",");
+ if (showUnique)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_UNIQUE) ",");
+ if (showExclusion)
+ appendPQExpBufferStr(&buf, CppAsString2(CONSTRAINT_EXCLUSION) ",");
+
+ appendPQExpBufferStr(&buf, " ''"); /* dummy */
+ appendPQExpBufferStr(&buf, ")\n");
+ }
+
+ appendPQExpBufferStr(&buf, "ORDER BY 1, 2;");
+
+ res = PSQLexec(buf.data);
+ termPQExpBuffer(&buf);
+ if (!res)
+ return false;
+
+ myopt.title = _("List of constraints");
+ myopt.translate_header = true;
+
+ printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+ PQclear(res);
+ return true;
+}
+
/*
* \dC
*
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 18ecaa60949..c389a9e38d5 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -83,6 +83,10 @@ extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
extern bool describeConfigurationParameters(const char *pattern, bool verbose,
bool showSystem);
+/* \dcs */
+extern bool listConstraints(const char *contypes, const char *pattern, bool verbose,
+ bool showSystem);
+
/* \dC */
extern bool listCasts(const char *pattern, bool verbose);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index ed00c36695e..d8a23d2c6fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -230,6 +230,8 @@ slashUsage(unsigned short int pager)
HELP0(" \\db[x+] [PATTERN] list tablespaces\n");
HELP0(" \\dc[Sx+] [PATTERN] list conversions\n");
HELP0(" \\dconfig[x+] [PATTERN] list configuration parameters\n");
+ HELP0(" \\dcs[cfnptue] [Sx+] [PATTERN] list [only check/foreign key/not-null/primary key\n"
+ " /constraint trigger/unique key/exclusion] constraints\n");
HELP0(" \\dC[x+] [PATTERN] list casts\n");
HELP0(" \\dd[Sx] [PATTERN] show object descriptions not displayed elsewhere\n");
HELP0(" \\dD[Sx+] [PATTERN] list domains\n");
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 36ea6a4d557..877e89beca5 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1913,7 +1913,7 @@ psql_completion(const char *text, int start, int end)
"\\connect", "\\conninfo", "\\C", "\\cd", "\\close_prepared", "\\copy",
"\\copyright", "\\crosstabview",
"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
- "\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+ "\\db", "\\dc", "\\dconfig", "\\dcs", "\\dC", "\\dd", "\\ddp", "\\dD",
"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -5353,6 +5353,8 @@ match_previous_words(int pattern_id,
COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces);
else if (TailMatchesCS("\\dconfig*"))
COMPLETE_WITH_QUERY_VERBATIM(Query_for_list_of_show_vars);
+ else if (TailMatchesCS("\\dcs*"))
+ COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_constraints_with_schema);
else if (TailMatchesCS("\\dD*"))
COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains);
else if (TailMatchesCS("\\des*"))
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index fa8984ffe0d..ee6379851c8 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -5356,6 +5356,277 @@ List of configuration parameters
(1 row)
reset work_mem;
+-- check \dco
+CREATE TABLE con_c (
+ primary_col SERIAL PRIMARY KEY
+);
+CREATE TABLE con_p (
+ primary_col SERIAL PRIMARY KEY,
+ notnull_col TEXT NOT NULL,
+ check_col INT CHECK (check_col >= 0),
+ foreign_col INT REFERENCES con_c(primary_col),
+ unique_col TEXT UNIQUE,
+ exclusion_col INT,
+ CONSTRAINT con_p_exclusion EXCLUDE USING btree (exclusion_col WITH =)
+);
+CREATE OR REPLACE FUNCTION trigger_hoge() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END;
+ $$ LANGUAGE PLPGSQL;
+CREATE CONSTRAINT TRIGGER con_p_trigger AFTER INSERT ON con_p
+ FOR EACH ROW EXECUTE PROCEDURE trigger_hoge();
+\dcs con_*
+ List of constraints
+ Schema | Name
+--------+----------------------------
+ public | con_c_pkey
+ public | con_c_primary_col_not_null
+ public | con_p_check_col_check
+ public | con_p_exclusion
+ public | con_p_foreign_col_fkey
+ public | con_p_notnull_col_not_null
+ public | con_p_pkey
+ public | con_p_primary_col_not_null
+ public | con_p_trigger
+ public | con_p_unique_col_key
+(10 rows)
+
+\dcscfnptue con_*
+ List of constraints
+ Schema | Name
+--------+----------------------------
+ public | con_c_pkey
+ public | con_c_primary_col_not_null
+ public | con_p_check_col_check
+ public | con_p_exclusion
+ public | con_p_foreign_col_fkey
+ public | con_p_notnull_col_not_null
+ public | con_p_pkey
+ public | con_p_primary_col_not_null
+ public | con_p_trigger
+ public | con_p_unique_col_key
+(10 rows)
+
+\dcs+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+----------------------------+---------------------------------------------------------+-------
+ public | con_c_pkey | PRIMARY KEY (primary_col) | con_c
+ public | con_c_primary_col_not_null | NOT NULL primary_col | con_c
+ public | con_p_check_col_check | CHECK ((check_col >= 0)) | con_p
+ public | con_p_exclusion | EXCLUDE USING btree (exclusion_col WITH =) | con_p
+ public | con_p_foreign_col_fkey | FOREIGN KEY (foreign_col) REFERENCES con_c(primary_col) | con_p
+ public | con_p_notnull_col_not_null | NOT NULL notnull_col | con_p
+ public | con_p_pkey | PRIMARY KEY (primary_col) | con_p
+ public | con_p_primary_col_not_null | NOT NULL primary_col | con_p
+ public | con_p_trigger | TRIGGER | con_p
+ public | con_p_unique_col_key | UNIQUE (unique_col) | con_p
+(10 rows)
+
+\dcscfnptue+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+----------------------------+---------------------------------------------------------+-------
+ public | con_c_pkey | PRIMARY KEY (primary_col) | con_c
+ public | con_c_primary_col_not_null | NOT NULL primary_col | con_c
+ public | con_p_check_col_check | CHECK ((check_col >= 0)) | con_p
+ public | con_p_exclusion | EXCLUDE USING btree (exclusion_col WITH =) | con_p
+ public | con_p_foreign_col_fkey | FOREIGN KEY (foreign_col) REFERENCES con_c(primary_col) | con_p
+ public | con_p_notnull_col_not_null | NOT NULL notnull_col | con_p
+ public | con_p_pkey | PRIMARY KEY (primary_col) | con_p
+ public | con_p_primary_col_not_null | NOT NULL primary_col | con_p
+ public | con_p_trigger | TRIGGER | con_p
+ public | con_p_unique_col_key | UNIQUE (unique_col) | con_p
+(10 rows)
+
+\dcsc con_*
+ List of constraints
+ Schema | Name
+--------+-----------------------
+ public | con_p_check_col_check
+(1 row)
+
+\dcsc+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+-----------------------+--------------------------+-------
+ public | con_p_check_col_check | CHECK ((check_col >= 0)) | con_p
+(1 row)
+
+\dcsf con_*
+ List of constraints
+ Schema | Name
+--------+------------------------
+ public | con_p_foreign_col_fkey
+(1 row)
+
+\dcsf+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+------------------------+---------------------------------------------------------+-------
+ public | con_p_foreign_col_fkey | FOREIGN KEY (foreign_col) REFERENCES con_c(primary_col) | con_p
+(1 row)
+
+\dcsn con_*
+ List of constraints
+ Schema | Name
+--------+----------------------------
+ public | con_c_primary_col_not_null
+ public | con_p_notnull_col_not_null
+ public | con_p_primary_col_not_null
+(3 rows)
+
+\dcsn+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+----------------------------+----------------------+-------
+ public | con_c_primary_col_not_null | NOT NULL primary_col | con_c
+ public | con_p_notnull_col_not_null | NOT NULL notnull_col | con_p
+ public | con_p_primary_col_not_null | NOT NULL primary_col | con_p
+(3 rows)
+
+\dcsp con_*
+ List of constraints
+ Schema | Name
+--------+------------
+ public | con_c_pkey
+ public | con_p_pkey
+(2 rows)
+
+\dcsp+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+------------+---------------------------+-------
+ public | con_c_pkey | PRIMARY KEY (primary_col) | con_c
+ public | con_p_pkey | PRIMARY KEY (primary_col) | con_p
+(2 rows)
+
+\dcst con_*
+ List of constraints
+ Schema | Name
+--------+---------------
+ public | con_p_trigger
+(1 row)
+
+\dcst+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+---------------+------------+-------
+ public | con_p_trigger | TRIGGER | con_p
+(1 row)
+
+\dcsu con_*
+ List of constraints
+ Schema | Name
+--------+----------------------
+ public | con_p_unique_col_key
+(1 row)
+
+\dcsu+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+----------------------+---------------------+-------
+ public | con_p_unique_col_key | UNIQUE (unique_col) | con_p
+(1 row)
+
+\dcse con_*
+ List of constraints
+ Schema | Name
+--------+-----------------
+ public | con_p_exclusion
+(1 row)
+
+\dcse+ con_*
+ List of constraints
+ Schema | Name | Definition | Table
+--------+-----------------+--------------------------------------------+-------
+ public | con_p_exclusion | EXCLUDE USING btree (exclusion_col WITH =) | con_p
+(1 row)
+
+\dcsS pg_constraint*
+ List of constraints
+ Schema | Name
+------------+-----------------------------------------------
+ pg_catalog | pg_constraint_conrelid_contypid_conname_index
+ pg_catalog | pg_constraint_oid_index
+ pg_catalog | pg_constraint_oid_not_null
+(3 rows)
+
+\dcsScfnpue pg_constraint*
+ List of constraints
+ Schema | Name
+------------+-----------------------------------------------
+ pg_catalog | pg_constraint_conrelid_contypid_conname_index
+ pg_catalog | pg_constraint_oid_index
+ pg_catalog | pg_constraint_oid_not_null
+(3 rows)
+
+\dcsS+ pg_constraint*
+ List of constraints
+ Schema | Name | Definition | Table
+------------+-----------------------------------------------+--------------------------------------+---------------
+ pg_catalog | pg_constraint_conrelid_contypid_conname_index | UNIQUE (conrelid, contypid, conname) | pg_constraint
+ pg_catalog | pg_constraint_oid_index | PRIMARY KEY (oid) | pg_constraint
+ pg_catalog | pg_constraint_oid_not_null | NOT NULL oid | pg_constraint
+(3 rows)
+
+\dcsScfnpue+ pg_constraint*
+ List of constraints
+ Schema | Name | Definition | Table
+------------+-----------------------------------------------+--------------------------------------+---------------
+ pg_catalog | pg_constraint_conrelid_contypid_conname_index | UNIQUE (conrelid, contypid, conname) | pg_constraint
+ pg_catalog | pg_constraint_oid_index | PRIMARY KEY (oid) | pg_constraint
+ pg_catalog | pg_constraint_oid_not_null | NOT NULL oid | pg_constraint
+(3 rows)
+
+-- test with search_path
+CREATE SCHEMA con_schema;
+CREATE TABLE con_schema.con_schema_test (
+ primary_col SERIAL PRIMARY KEY
+);
+SET SEARCH_PATH TO public, con_schema;
+\dcs con_*
+ List of constraints
+ Schema | Name
+------------+--------------------------------------
+ con_schema | con_schema_test_pkey
+ con_schema | con_schema_test_primary_col_not_null
+ public | con_c_pkey
+ public | con_c_primary_col_not_null
+ public | con_p_check_col_check
+ public | con_p_exclusion
+ public | con_p_foreign_col_fkey
+ public | con_p_notnull_col_not_null
+ public | con_p_pkey
+ public | con_p_primary_col_not_null
+ public | con_p_trigger
+ public | con_p_unique_col_key
+(12 rows)
+
+SET SEARCH_PATH TO public;
+\dcs con_*
+ List of constraints
+ Schema | Name
+--------+----------------------------
+ public | con_c_pkey
+ public | con_c_primary_col_not_null
+ public | con_p_check_col_check
+ public | con_p_exclusion
+ public | con_p_foreign_col_fkey
+ public | con_p_notnull_col_not_null
+ public | con_p_pkey
+ public | con_p_primary_col_not_null
+ public | con_p_trigger
+ public | con_p_unique_col_key
+(10 rows)
+
+RESET search_path;
+-- clean up
+DROP SCHEMA con_schema CASCADE;
+NOTICE: drop cascades to table con_schema.con_schema_test
+DROP TABLE con_p, con_c CASCADE;
+DROP FUNCTION trigger_hoge;
-- check \df, \do with argument specifications
\df *sqrt
List of functions
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index f064e4f5456..5f5adc9615e 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -1331,6 +1331,70 @@ set work_mem = 10240;
\dconfig+ work*
reset work_mem;
+-- check \dco
+CREATE TABLE con_c (
+ primary_col SERIAL PRIMARY KEY
+);
+
+CREATE TABLE con_p (
+ primary_col SERIAL PRIMARY KEY,
+ notnull_col TEXT NOT NULL,
+ check_col INT CHECK (check_col >= 0),
+ foreign_col INT REFERENCES con_c(primary_col),
+ unique_col TEXT UNIQUE,
+ exclusion_col INT,
+ CONSTRAINT con_p_exclusion EXCLUDE USING btree (exclusion_col WITH =)
+);
+
+CREATE OR REPLACE FUNCTION trigger_hoge() RETURNS TRIGGER AS $$
+ BEGIN
+ RETURN NULL;
+ END;
+ $$ LANGUAGE PLPGSQL;
+
+CREATE CONSTRAINT TRIGGER con_p_trigger AFTER INSERT ON con_p
+ FOR EACH ROW EXECUTE PROCEDURE trigger_hoge();
+
+\dcs con_*
+\dcscfnptue con_*
+\dcs+ con_*
+\dcscfnptue+ con_*
+\dcsc con_*
+\dcsc+ con_*
+\dcsf con_*
+\dcsf+ con_*
+\dcsn con_*
+\dcsn+ con_*
+\dcsp con_*
+\dcsp+ con_*
+\dcst con_*
+\dcst+ con_*
+\dcsu con_*
+\dcsu+ con_*
+\dcse con_*
+\dcse+ con_*
+\dcsS pg_constraint*
+\dcsScfnpue pg_constraint*
+\dcsS+ pg_constraint*
+\dcsScfnpue+ pg_constraint*
+
+-- test with search_path
+CREATE SCHEMA con_schema;
+CREATE TABLE con_schema.con_schema_test (
+ primary_col SERIAL PRIMARY KEY
+);
+
+SET SEARCH_PATH TO public, con_schema;
+\dcs con_*
+SET SEARCH_PATH TO public;
+\dcs con_*
+RESET search_path;
+
+-- clean up
+DROP SCHEMA con_schema CASCADE;
+DROP TABLE con_p, con_c CASCADE;
+DROP FUNCTION trigger_hoge;
+
-- check \df, \do with argument specifications
\df *sqrt
\df *sqrt num*
--
2.43.5
Hi Tatsuro
On 31/10/2025 16:19, Tatsuro Yamada wrote:
Please find the attached file.
Feedback and suggestions are very welcome.
Thanks for the patch!
I've been playing with a few edge cases and everything seems to work
just fine. Bellow I am listing the cases I tested, so that you can take
a look if some of them should be included in the regression tests - not
sure if it is necessary, since this feature is only reading the
constraint definitions from the catalog.
CREATE TABLE zoo (
cage int,
animal text,
is_aggressive boolean,
CONSTRAINT no_different_animals_in_same_cage
EXCLUDE USING gist (
cage WITH =,
animal WITH <>,
int4(is_aggressive) WITH <>
)
);
CREATE TABLE
postgres=# \dcs+
List of constraints
Schema | Name |
Definition | Table
--------+-----------------------------------+-------------------------------------------------------------------------------+-------
public | no_different_animals_in_same_cage | EXCLUDE USING gist (cage
WITH =, animal WITH <>, int4(is_aggressive) WITH <>) | zoo
(1 row)
postgres=# CREATE TABLE zoo_partial (
cage int,
animal text,
is_aggressive boolean,
CONSTRAINT zoo_partial_excl
EXCLUDE USING gist (
cage WITH =,
animal WITH <>
)
WHERE (is_aggressive)
);
CREATE TABLE
postgres=# \dcs+ zoo_partial*
List of constraints
Schema | Name | Definition
| Table
--------+------------------+------------------------------------------------------------------------+-------------
public | zoo_partial_excl | EXCLUDE USING gist (cage WITH =, animal
WITH <>) WHERE (is_aggressive) | zoo_partial
(1 row)
postgres=# CREATE TABLE zoo_deferrable (
cage int,
animal text,
CONSTRAINT zoo_deferrable_excl
EXCLUDE USING gist (
cage WITH =,
animal WITH <>
)
DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE
postgres=# \dcs+ zoo_deferrable_excl
List of constraints
Schema | Name |
Definition | Table
--------+---------------------+--------------------------------------------------------------------------------+----------------
public | zoo_deferrable_excl | EXCLUDE USING gist (cage WITH =, animal
WITH <>) DEFERRABLE INITIALLY DEFERRED | zoo_deferrable
(1 row)
postgres=# CREATE TABLE zoo_expr (
cage int,
animal text,
is_aggressive boolean,
CONSTRAINT zoo_expr_excl
EXCLUDE USING gist (
cage WITH =,
lower(animal) WITH =,
int4(is_aggressive) WITH <>
)
);
CREATE TABLE
postgres=# \dcs+ zoo_expr_excl
List of constraints
Schema | Name | Definition
| Table
--------+---------------+-------------------------------------------------------------------------------------+----------
public | zoo_expr_excl | EXCLUDE USING gist (cage WITH =, lower(animal)
WITH =, int4(is_aggressive) WITH <>) | zoo_expr
(1 row)
postgres=# CREATE TABLE zoo_check (
cage int,
animal text,
is_aggressive boolean
);
ALTER TABLE zoo_check
ADD CONSTRAINT zoo_check_aggr
CHECK (is_aggressive IS NOT NULL)
NOT VALID;
CREATE TABLE
ALTER TABLE
postgres=# \dcs+ zoo_check_aggr
List of constraints
Schema | Name | Definition
| Table
--------+----------------+-----------------------------------------------+-----------
public | zoo_check_aggr | CHECK ((is_aggressive IS NOT NULL)) NOT VALID
| zoo_check
(1 row)
postgres=# CREATE TABLE zoo_parent (
cage int PRIMARY KEY
);
CREATE TABLE zoo_child (
animal text
) INHERITS (zoo_parent);
CREATE TABLE
CREATE TABLE
postgres=# \dcs+ zoo_parent_cage_not_null
List of constraints
Schema | Name | Definition | Table
--------+--------------------------+---------------+------------
public | zoo_parent_cage_not_null | NOT NULL cage | zoo_parent
public | zoo_parent_cage_not_null | NOT NULL cage | zoo_child
(2 rows)
CREATE TABLE zoo_part (
cage int,
animal text,
CONSTRAINT zoo_part_pk PRIMARY KEY (cage)
) PARTITION BY RANGE (cage);
CREATE TABLE
db=# CREATE TABLE zoo_part_1
PARTITION OF zoo_part
FOR VALUES FROM (0) TO (100);
CREATE TABLE
db=# \dcs+
List of constraints
Schema | Name | Definition | Table
--------+------------------------+--------------------+------------
public | zoo_part_1_pkey | PRIMARY KEY (cage) | zoo_part_1
public | zoo_part_cage_not_null | NOT NULL cage | zoo_part
public | zoo_part_cage_not_null | NOT NULL cage | zoo_part_1
public | zoo_part_pk | PRIMARY KEY (cage) | zoo_part
(4 rows)
postgres=# drop table zoo_drop ;
DROP TABLE
postgres=# CREATE TABLE zoo_drop (
cage int,
animal text,
CONSTRAINT zoo_drop_uq UNIQUE (cage, animal)
);
CREATE TABLE
postgres=# \dcs+
List of constraints
Schema | Name | Definition | Table
--------+-------------+-----------------------+----------
public | zoo_drop_uq | UNIQUE (cage, animal) | zoo_drop
(1 row)
postgres=# ALTER TABLE zoo_drop DROP COLUMN animal;
ALTER TABLE
postgres=# \dcs+
List of constraints
Schema | Name | Definition | Table
--------+------+------------+-------
(0 rows)
One nitpick: the order of the constraints is different from the one in \d+:
postgres=# CREATE TABLE t (
c text NOT NULL,
b text NOT NULL,
a text NOT NULL
);
CREATE TABLE
postgres=# \d+ t
Table "public.t"
Column | Type | Collation | Nullable | Default | Storage | Compression
| Stats target | Description
--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
c | text | | not null | | extended |
| |
b | text | | not null | | extended |
| |
a | text | | not null | | extended |
| |
Not-null constraints:
"t_c_not_null" NOT NULL "c"
"t_b_not_null" NOT NULL "b"
"t_a_not_null" NOT NULL "a"
Access method: heap
postgres=# \dcs+ t*
List of constraints
Schema | Name | Definition | Table
--------+--------------+------------+-------
public | t_a_not_null | NOT NULL a | t
public | t_b_not_null | NOT NULL b | t
public | t_c_not_null | NOT NULL c | t
(3 rows)
For consistency, it would be nice to have both options listing in the
same order, but in case it would mean adding too much complexity to the
code, I'd say it is just fine as-is.
Thanks!
Best, Jim
Hi Jim,
Thank you for your review.
On Sat, Jan 3, 2026 at 4:51 AM Jim Jones <jim.jones@uni-muenster.de> wrote:
I've been playing with a few edge cases and everything seems to work
just fine. Bellow I am listing the cases I tested, so that you can take
a look if some of them should be included in the regression tests - not
sure if it is necessary, since this feature is only reading the
constraint definitions from the catalog.
The original regression tests only covered simple cases, so the more
complex edge cases you tried were very helpful.
The method for converting constraints into definition strings in \dcs uses
the same function as \d (i.e., pg_catalog.pg_get_constraintdef), so I don’t
think there are any major issues there.
One nitpick: the order of the constraints is different from the one in \d+:
For consistency, it would be nice to have both options listing in the
same order, but in case it would mean adding too much complexity to the
code, I'd say it is just fine as-is.
Regarding the constraint display order, I hadn’t noticed that it differed
between \d and \dcs. Thanks for pointing that out.
In \dcs, constraints are sorted in ascending order by both schema name
and constraint name.
On the other hand, \d appears to use the column number as one of the
sorting keys. I believe this difference explains the discrepancy in the
display order.
As you suggested, I’d like to keep the current behavior to avoid adding
unnecessary complexity.
The next patch will include the following:
- Rebased version
- Expanded regression tests (kept to a minimum)
Regards,
Tatsuro Yamada