Emacsのsql-modeでDockerを経由してSupabaseに接続した時の覚書

はじめに

EmacsからSupabaseのPostgreSQLに接続したかった。 sql-mode には sql-postgres という組み込みのPostgreSQLバックエンドがあるが、今回はDockerコンテナ内の psql を使いたいという事情があった。 sql-postgres はローカルの psql コマンドを直接起動する前提なので、Docker経由で接続するにはそのまま使えない。

結局、 sql-mode にカスタムバックエンド(product)を追加する方法に落ち着いた。 sql-add-product でバックエンドを定義し、comint関数を自前で書くことで、Docker経由の接続を sql-mode のエコシステムに自然に統合できる。

使用するEmacsの関数と変数

今回の実装で使用するEmacsが提供する主な関数と変数を先にまとめておく。

sql.el(sql-mode)が提供するもの

sql-add-product
新しいSQLバックエンド(product)を sql-product-alist に登録する関数。product名、表示名、および :sqli-program:sqli-comint-func などのプロパティをキーワード引数で受け取る。
sql-del-product
登録済みのproductを sql-product-alist から削除する関数。
sql-comint
productに対応するcomintバッファを作成し、 :sqli-program で指定されたコマンドをコマンドライン引数のリストとともに起動する関数。カスタムの :sqli-comint-func の中から呼び出して使う。
sql-product-alist
登録されているすべてのproductの定義を保持する連想リスト。組み込みのPostgreSQL、MySQL等もここに格納されている。
sql-connection-alist
接続先の名前と接続パラメータの組を保持する連想リスト。 M-x sql-connect で名前を選ぶと、この定義に基づいて接続が行われる。
sql-interactive-mode-hook
SQLの対話バッファ( *SQL* バッファ)が作成されたときに実行されるフック。バッファローカルな設定やフィルタの追加に使う。
sql-server / sql-port / sql-user / sql-database
現在の接続先情報を保持するバッファローカル変数。 sql-connection-alist の値や対話的な入力によってセットされる。

comint.el(comintモード)が提供するもの

sql-mode の対話バッファは内部的にcomintモードの上に構築されている。

comint-preoutput-filter-functions
プロセスからの出力がバッファに挿入される前に呼ばれるフィルタ関数のリスト。各関数は出力文字列を受け取り、変換後の文字列を返す。出力の加工やインターセプトに使う。
comint-password-prompt-regexp
パスワードプロンプトにマッチする正規表現。 "Password:""Enter passphrase:" など、一般的なパスワードプロンプトのパターンがあらかじめ定義されている。

auth-source.el(認証情報管理)が提供するもの

auth-source-pick-first-password
~/.authinfo~/.authinfo.gpg などの認証情報ストアから、 :host:user をキーにしてパスワードを検索し、最初に見つかったものを返す関数。

Emacsコア関数

process-send-string
指定したプロセスに文字列を送信する関数。ここではcomintバッファのプロセスにパスワード文字列を送るために使っている。
get-buffer-process
バッファに関連付けられたプロセスを返す関数。

カスタムproductの登録

sql-mode では各データベースを「product」という単位で管理している。PostgreSQL、MySQL、SQLiteなどが組み込みで用意されているが、 sql-add-product を使えば独自のproductを追加できる。

(defvar sql-supabase-program "docker"
  "Command to start Supabase SQL connection via Docker.")

(defvar sql-supabase-login-params '(user server port database)
  "Login parameters for Supabase.")

(when (assoc 'supabase sql-product-alist)
  (sql-del-product 'supabase))
(sql-add-product 'supabase "Supabase"
                 :sqli-program 'sql-supabase-program
                 :sqli-login 'sql-supabase-login-params
                 :prompt-regexp "^Supabase> "
                 :prompt-cont-regexp "^Supabase: "
                 :sqli-comint-func 'my/sql-comint-supabase)

sql-add-product に渡す主なプロパティは以下のとおり。

:sqli-program
起動するコマンド。ここでは docker を指定している。
:sqli-login
接続時にユーザーに尋ねるパラメータのリスト。
:prompt-regexp / :prompt-cont-regexp
psql のプロンプトを認識するための正規表現。comintがプロンプトを正しく認識するために必要。
:sqli-comint-func
comintバッファを作成してプロセスを起動する関数。ここが実装の本体になる。

sql-del-product で既存の定義を削除してから追加しているのは、設定を eval-buffer で再評価したときに重複登録を防ぐためだ。

comint関数の実装

カスタムバックエンドの核心はcomint関数にある。この関数が docker run 経由で psql を起動する。

(defun my/sql-comint-supabase (product options &optional buf-name)
  "Create comint buffer and connect to Supabase via Docker."
  (sql-comint product
              (append '("run" "--rm" "-i"
                        "--entrypoint" "psql"
                        "sximada/agentic/all:202605.dev1"
                        "-P" "pager=off"
                        "--set" "PROMPT1=Supabase> "
                        "--set" "PROMPT2=Supabase: "
                        "--password")
                      (when (not (string= "" sql-server)) (list "-h" sql-server))
                      (when (and (numberp sql-port) (not (= 0 sql-port)))
                        (list "--port" (number-to-string sql-port)))
                      (when (not (string= "" sql-user)) (list "--username" sql-user))
                      (when (not (string= "" sql-database)) (list sql-database))
                      options)
              buf-name))

sql-comint はcomintバッファを作成してプロセスを起動する汎用関数で、引数としてコマンドライン引数のリストを受け取る。ここでは docker run のオプションと psql のオプションを組み立てている。

ポイントをいくつか挙げる。

--rm -i
コンテナを使い捨てにし、標準入力をアタッチする。 -t (TTY割り当て)は付けない。comintとの相性が悪いためだ。
--entrypoint psql
コンテナのデフォルトエントリーポイントを上書きして psql を直接起動する。
-P pager=off
ページャを無効にする。comintバッファ内でページャが動くと出力がおかしくなるため必須。
--set PROMPT1 / --set PROMPT2
psql のプロンプトを Supabase> / Supabase: に変更する。 sql-add-product で指定した :prompt-regexp と一致させる必要がある。
--password
psql にパスワード入力を強制させる。後述の自動入力フックと組み合わせて使う。

接続情報の定義

sql-connection-alist に接続先の情報を登録しておくと、 M-x sql-connect で名前を選ぶだけで接続できるようになる。

(setq sql-connection-alist
      `((supabase
         (sql-product 'supabase)
         (sql-server "YOUR_SUPABASE_HOST")
         (sql-port 5432)
         (sql-database "postgres")
         (sql-user "YOUR_USERNAME"))))

M-x sql-connect RET supabase RET で接続できる。

パスワードの自動入力

psql--password オプションが指定されているとパスワードプロンプトを出す。これをcomintの出力フィルタで検出し、 auth-source からパスワードを取得して自動入力する。

(add-hook 'sql-interactive-mode-hook
          (lambda ()
            (when (eq sql-product 'supabase)
              (add-hook 'comint-preoutput-filter-functions
                        (lambda (output)
                          (if (let ((case-fold-search t))
                                (string-match comint-password-prompt-regexp output))
                              (let ((password (auth-source-pick-first-password
                                              :host sql-server
                                              :user sql-user)))
                                (when password
                                  (process-send-string
                                   (get-buffer-process (current-buffer))
                                   (concat password "\n")))
                                "")
                            (if (string-match "\n\\'" output)
                                (concat output "Supabase> ")
                              output)))
                        nil t))))

このフックは2つの役割を持っている。

パスワードプロンプトの検出と自動入力
comint-password-prompt-regexp にマッチする出力が来たら、 auth-source-pick-first-password でパスワードを取得してプロセスに送信する。パスワードプロンプト自体はバッファに表示しない(空文字列を返す)。
プロンプトの補完
Docker経由だと psql の出力にプロンプトが含まれないケースがある。改行で終わる出力の末尾に Supabase> を付加することで、comintがプロンプトを正しく認識できるようにしている。

パスワードは ~/.authinfo~/.authinfo.gpg に保存しておく。

machine YOUR_SUPABASE_HOST login YOUR_USERNAME password YOUR_PASSWORD

まとめ

sql-mode のカスタムバックエンドは sql-add-product と comint関数を実装するだけで作れる。今回のようにDocker経由で接続したい場合や、独自のラッパースクリプトを噛ませたい場合に有用だ。 sql-connection-alist との組み合わせで接続操作も M-x sql-connect に統一でき、 auth-source との連携でパスワード管理も既存のEmacsの仕組みに乗せられる。

作成日