Emacsでリレーショナルデータベースを扱う

Emacsに梱包されている lisp/progmodes/sql.el は、リレーショナルデータベースを扱うための機能を提供している。それらの殆どは独立して動くのではなく、リレーショナルデータベースへアクセスするクライアント用のコマンドをサブプロセスとして起動し、そのサブプロセスとやりとりする事で、リレーショナルデータベスを操作する。今回は、それらの機能について見ていく事にする。

サーポートしているデータベース

Emacsは様々なRDBMSに対する機能をサポートしている。サポートしているRDBMSの一覧を確認する。

(mapcar (lambda (m) (car m)) sql-product-alist)

(ansi db2 informix ingres interbase linter mariadb ms mysql oracle postgres solid sqlite sybase vertica)

今でも使われるMySQLやPostgreSQLもあれば、最近ではあまり使われる事例を聞かないinformixやingresなどもサポートしている。

Amazon Redshift、Google BigQueryについては機能を提供していなかった。TiDBのサポートも無い。

RDBMSではないけれど、CassandraについてはELPAに登録されていた。Amazon DynamoDBは無かった。Google CloudSpannerも無かった。Snowflakeも無い。Elasticseachについてはes-modeがELPAに登録されているけれど、使ってみていないのでどのような代物なのかは分からない。Apache Solrも無い。Amazon Cloudwatch Logsのクエリや、Datadogのログ検索用のクエリについても無い。ただし、仕組みだけを考えるとCLIツールが提供されていれば、I/Fを実装するのは、それほど難しくないようにも思う。

RDBMSに接続する

基本的な変数を設定した後、各種RDBMSの接続コマンドを実行する事で接続できる。

基本的な変数

  • sql-user
  • sql-password
  • sql-database
  • sql-server
  • sql-port

特にパスワードなどは直接書くのではなく、GnuPGで暗号化したファイルに記述し、そのファイルをsql-password-walletに指定する。それらをsql-get-loginやsql-get-login-extなどで取得するのが良さそうではあるけれど、かなり複雑なのと、KeePassXCなど他のソフトウェアと連携するために、自分で実装する事もできる。

各種RDBMSに接続するためにはクライアントとなるCLIコマンドが必要になる。

MySQLに接続する

先程の情報を使用し接続する。

M-x sql-mysql

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-mysql-program
  • sql-mysql-options
  • sql-mysql-login-params

PostgreSQLに接続する

先程の情報を使用し接続する。

M-x sql-postgres

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-postgres-program
  • sql-postgres-options
  • sql-postgres-login-params

MariaDBに接続する

先程の情報を使用し接続する。

M-x sql-mariadb

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-mariadb-program
  • sql-mariadb-options
  • sql-mariadb-login-params

IBM Db2に接続する

先程の情報を使用し接続する。

M-x sql-db2

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-db2-program
  • sql-db2-options
  • sql-db2-login-params
  • sql-db2-escape-newlines

Microsonft SQL Serverに接続する

先程の情報を使用し接続する。

M-x sql-ms

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-ms-program
  • sql-ms-options
  • sql-ms-login-params

このコマンドはosqlを使用して、RDBMSに接続する。使用するコマンドは sql-ms-program で、コマンドに渡す引数は sql-ms-options で指摘する。

Oracle Databaseに接続する

先程の情報を使用し接続する。

M-x sql-oracle

さらに、以下の変数でログイン時の挙動を指定できる。

  • sql-oracle-program
  • sql-oracle-options
  • sql-oracle-login-params

Dockerで動いているRDBMSのコンテナに直接入って接続する

RDBMSをコンテナとして動作させる事もあるだろう。例えば開発時にローカルホストでコンテナとしてRDBMSを動作させていて、ローカルホストにはRDBMSのクライアントを入れていないとする。

先程のログイン関連の変数を変更する事で、Emacsから接続する事もできる。本来はクライアントを起動するコンテナを別途開始するべきかもしれないが、今回は docker exec を使って直接RDBMSが動作しているコンテナに入り、接続する事にする。

PostgreSQL

(setq sql-postgres-program "docker")
(setq sql-postgres-options '("exec" "-ti" "postgres" "psql" "-U" "postgres"))
(setq sql-postgres-login-params nil)

IBM Db2

localで使うDb2の環境をDockerを用いて構築している。そのためDb2サーバへの接続はコンテナに直接入る方法を用いることにした。sql-db2-programとsql-db2-optionsを設定する。

(setq sql-db2-program "docker")
(setq sql-db2-options '("exec" "-ti" "db2-server" "bash" "-c" "su - db2inst1 -c 'db2 -t'"))

Db2は改行でSQLを実行する。その場合複数行にまたがるSQLは改行を削除して実行する必要がある。 -t を指定すると改行ではなくセミコロン(;)でSQLを実行するようになる。

接続する前には必ずDB2サーバコンテナを起動しておく必要がある。起動するためのスクリプトはwhalebrewディレクトリにある。

https://github.com/TakesxiSximada/emacs.d/blob/main/whalebrew/db2-server

Microsonft SQL Server

MSSQL Serverについてはsql-modeでの扱いが難しい。

https://www.emacswiki.org/emacs/SqlMode#h5o-8

そこでDocker経由で接続するためのコマンドを別途用意する。

(defun sql-docker-ms ()
  (interactive)
  (let ((vterm-shell "docker exec -ti mssql bash -c '/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P ${SA_PASSWORD}'")
	(vterm-buffer-name "SQL: Docker: Microsoft MSSQL Server")
	(vterm-kill-buffer-on-exit nil))
  (vterm)))

Microsoft SQL Server

(setq sql-ms-program "docker")
(setq sql-ms-options '("exec" "-ti" "mssql" "/opt/mssql-tools/bin/sqlcmd"))
(setq sql-ms-login-params '(user server))

" "-S" "localhost" "-U" "SA" "-P" "Mypassword1!" (setq sql-server "localhost") (setq sql-user "SA") (setq sql-password "Mypassword1!")

Oracle

mypasswor はパスワード、 oracle はコンテナ名とする。

(setq sql-oracle-program "docker")
(setq sql-oracle-options '("exec" "-ti" "oracle" "bash" "-c" "sqlplus sys/mypassword@localhost:1521/ORCLCDB as sysdba"))
(setq sql-oracle-login-params nil)

SQLフォーマッターの設定

以下を参考にした。


(defvar sql-fmt-command "sqlfluff fix --dialect mysql --config ~/.emacs.d/.sqlfluff -")

(defun sql-fmt-region (beg end)
  (interactive "r")
  (save-restriction
    (shell-command-on-region beg end
			     sql-fmt-command
			     nil t)))

(defun sql-fmt-buffer ()
  (interactive)
  (sql-fmt-region (point-min) (point-max)))

SQLを編集しながらEXPLAINする

SQLをチューニングしていると即座にEXPLAINして欲しくなる。アイドルタイマーとフック、一時バッファとしてサードパーティパッケージであるedit-indirectを利用し、SQLのEXPLAINを確認できるようにした。ソースを置いておく。パッケージで配布する程度のものではないのでMELPAには上げていない。

使用する時は、このEmacs Lispをダウンロードし、load-pathにディレクトリのパスを追加してrequireするか、直接ファイルをloadするか、ファイルを開いてeva-bufferする。

sql-focus.el

;;; sql-focus --- Focus on that SQL -*- lexical-binding: t -*-

;; Copyright (C) 2023 TakesxiSximada

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Repository:
;; Version: 1
;; Package-Version: 20230331.0000
;; Package-Requires: ((emacs "28.0")
;; Date: 2023-03-31

;; This file is part of sql-focus.

;; sql-focus is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.

;; sql-focus is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Code:

(require 'edit-indirect)

(defvar-local sql-focus-hook nil)
(defvar sql-focus-delay 1)
(defvar sql-focus-timer nil)
(defvar sql-focus-explain-buffer \"*SQL: Postgres*\")

(defun sql-focus-get-explain-sql ()
  (format "EXPLAIN %s;\n"
	  (buffer-substring-no-properties (point-min) (point-max))))

;;;###autoload
(defun sql-focus-explain ()
  (interactive)
  (let ((proc (get-buffer-process (get-buffer sql-focus-explain-buffer))))
    (comint-send-string proc "\n")
    (comint-send-string proc (sql-focus-get-explain-sql))))

;;;###autoload
(defun sql-focus-activate ()
  (interactive)
  (add-hook 'sql-focus-hook #'sql-focus-explain nil t)
  (when (timerp sql-focus-timer)
    (cancel-timer sql-focus-timer))
  (setq sql-focus-timer (run-with-idle-timer sql-focus-delay t #'run-hooks 'sql-focus-hook)))

;;;###autoload
(defun sql-focus-deactivate ()
  (interactive)
  (remove-hook 'sql-focus-hook #'sql-focus-explain t)
  (when (timerp sql-focus-timer)
    (cancel-timer sql-focus-timer)))

;;;###autoload
(defun sql-focus (beg end)
  (interactive "r")
  (let ((buf (edit-indirect-region beg end t)))
    (with-current-buffer buf
      (sql-focus-activate))
    (display-buffer sql-focus-explain-buffer '(display-buffer-below-selected))))

(provide 'sql-focus)
;;; sql-focus ends here

こういった処理は、それぞれのエディタで用意されているが、何がどうなって欲しいかは、その時々で結構異なる事が多い。「こうしたいな」というイメージが浮んだ時に、すぐにエディタを拡張し、やりたいことを実現できる所がEmacsの良い所だと思っている。