はじめに
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の仕組みに乗せられる。