psqlで\dpコマンドが使えない場合の対処法

\dp コマンドは、PostgreSQL のテーブル、ビュー、シーケンスのアクセス権一覧を表示するためのコマンドである。 しかし、PostgreSQL 15 を利用している場合に以下のようなエラーが出て失敗してしまうことがある。

postgres=# \dp
ERROR:  operator is not unique: unknown || "char"
LINE 16:            E' (' || polcmd || E'):'
                          ^
HINT:  Could not choose a best candidate operator. You might need to add explicit type casts.

事象としては過去に書いた以下記事と同じようなものであるが、原因について調べてみた。

gumfum.hatenablog.com

原因

サーバ側の PostgreSQL のバージョンが 15 で、クライアント側の psql のバージョンがそれ以前の場合に発生することがある。私の手元で上記のエラーメッセージが出た際、サーバ側のバージョンは 15.3、クライアント側のバージョンは 14.7 であった。

% psql --version
psql (PostgreSQL) 14.7 (Homebrew)

接続する際にも、psql の機能が動かない可能性がある旨の WARNING が出る。

psql (14.7 (Homebrew), server 15.3 (Debian 15.3-1.pgdg120+1))
WARNING: psql major version 14, server major version 15.
         Some psql features might not work.
Type "help" for help.

サーバ側のログにも同様のエラーと詳細が出ている。

2023-11-28 18:43:35.801 UTC [241] ERROR:  operator is not unique: unknown || "char" at character 770
2023-11-28 18:43:35.801 UTC [241] HINT:  Could not choose a best candidate operator. You might need to add explicit type casts.
2023-11-28 18:43:35.801 UTC [241] STATEMENT:  SELECT n.nspname as "Schema",
      c.relname as "Name",
      CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'm' THEN 'materialized view' WHEN 'S' THEN 'sequence' WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' END as "Type",
      pg_catalog.array_to_string(c.relacl, E'\n') AS "Access privileges",
      pg_catalog.array_to_string(ARRAY(
        SELECT attname || E':\n  ' || pg_catalog.array_to_string(attacl, E'\n  ')
        FROM pg_catalog.pg_attribute a
        WHERE attrelid = c.oid AND NOT attisdropped AND attacl IS NOT NULL
      ), E'\n') AS "Column privileges",
      pg_catalog.array_to_string(ARRAY(
        SELECT polname
        || CASE WHEN NOT polpermissive THEN
           E' (RESTRICTIVE)'
           ELSE '' END
        || CASE WHEN polcmd != '*' THEN
               E' (' || polcmd || E'):'
           ELSE E':'
           END
        || CASE WHEN polqual IS NOT NULL THEN
               E'\n  (u): ' || pg_catalog.pg_get_expr(polqual, polrelid)
           ELSE E''
           END
        || CASE WHEN polwithcheck IS NOT NULL THEN
               E'\n  (c): ' || pg_catalog.pg_get_expr(polwithcheck, polrelid)
           ELSE E''
           END    || CASE WHEN polroles <> '{0}' THEN
               E'\n  to: ' || pg_catalog.array_to_string(
                   ARRAY(
                       SELECT rolname
                       FROM pg_catalog.pg_roles
                       WHERE oid = ANY (polroles)
                       ORDER BY 1
                   ), E', ')
           ELSE E''
           END
        FROM pg_catalog.pg_policy pol
        WHERE polrelid = c.oid), E'\n')
        AS "Policies"
    FROM pg_catalog.pg_class c
         LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
    WHERE c.relkind IN ('r','v','m','S','f','p')
      AND n.nspname !~ '^pg_' AND pg_catalog.pg_table_is_visible(c.oid)
    ORDER BY 1, 2;

これは PostgreSQL 内部で利用される文字の型が、PostgreSQL 15 から一部変わった影響のようである。このため以前のバージョンの psql からコマンドを実行した場合、型エラーとして処理されてしまう場合があり、これに \dp コマンドが該当してしまったためと言えそうでである。
参考: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=07eee5a0dc642d26f44d65c4e6263304208e8583

対処法

1. クライアント側の psql のバージョンをサーバ側の PostgreSQL のバージョンと合わせる

基本的にはクライアント側のpsqlのバージョンを、サーバ側のPostgreSQLのバージョンと合わせることで解消するしかない。Homebrew でインストールしている場合は、@15 でバージョンを指定した上で、パスを通す必要がある。

% brew install postgresql@15
% export PATH=/usr/local/Cellar/postgresql@15/15.5_1/bin:$PATH
% psql --version
psql (PostgreSQL) 15.5 (Homebrew)

クライアント側とサーバ側のメジャーバージョンが合ったことにより、\dp コマンドが実行できるようになった。

% psql -h localhost -p 6432 -U postgres
ユーザー postgres のパスワード: 
psql (15.5 (Homebrew)、サーバー 15.3 (Debian 15.3-1.pgdg120+1))
"help"でヘルプを表示します。

postgres=# \dp
                         アクセス権限
 スキーマ | 名前 | タイプ | アクセス権限 | 列の権限 | ポリシー 
----------+------+--------+--------------+----------+----------
(0 行)

2. システムカタログを直接参照する

\dp コマンドで確認できるテーブル、ビュー、シーケンスのアクセス権一覧を、PostgreSQL のシステムカタログから直接参照する方法である。pg_class.relname がテーブル、インデックス、ビューなどの名前 、pg_namespace.nspacl がアクセス権限を表すカラムとなる。

SELECT
    relname, relacl
FROM
    pg_class c
JOIN
    pg_namespace ns ON (ns.oid = c.relnamespace)
WHERE
    ns.nspname = '<スキーマ名>';

スキーマ名を指定しない場合は、システムにより自動作成されたTOASTテーブルも一覧に出てきてしまうため、ある程度の絞り込みとしてスキーマ名は指定するのが良さそうである。また、テーブルのみなどのより詳細な絞り込みを行いたい場合は、pg_namespace.relname で指定するか、他のシステムカタログと組み合わせるなどする必要がある。

\dp コマンドの情報も大元はシステムカタログで管理されているものであるため、SQL を組み立てることで同様の情報を参照することはできるが、\dp コマンドの方が手軽かつ情報が見やすく整理されているため、可能であればクライアント側のバージョンを上げてしまうのが良さそうである。