EmacSQL 简介
我为这个 Emacs package 已经花费了几周的时间了。EmacSQL 是一个 Emacs 上的高层 SQL 数据库抽象接口。它主要使用 SQLite 作为后端,目前也支持 PostgreSQL 和 MySQL.
该 package 可以 通过 MELPA 安装 ,并且安装好后立即就能用了。它依赖于我上周才添加的 finalizers package .
虽然这个 package 依赖于一个非 Elisp 的组件,SQLite,但对于使用者来说,并不需要关心这个。当编译该 package 的 Elisp 的时候,若系统已经安装了 C 编译器,则 package 会自动编译出 SQLite 执行程序共 EmacSQL 使用。否则,会自动下载我预编译好的 SQLite 执行程序。理想情况下,EmacSQL 的这部分非 Elisp 组件可以对使用者完全透明,使用者完全可以认为 Emacs 已经内建了关系型数据库。
EmacSQL 不会去用 SQLite 的官方命令行程序(即使已经有了也不会取用),原因我会随后解释。
就好像 Skewer 使我接触到 web 开发一样,EmacSQL 让我快速学到了很多 SQL 与关系型数据库的知识。在开始这个项目前,我对这个领域知之甚少,但在开发这个项目的过程中,我学到了许多这方面的知识。创建一个 Emacs 扩展真是进入一门新领域的快速途径。
如果你跟我一样完全是个新手,而你又想自学 SQLite 的 SQL,我强烈推荐 Using SQLite 这篇文章。这真是一篇入门精品。
High-level SQL Compiler
所谓“high-level”意味着它会帮你拼接 SQL 语句。EmacSQL 是根据一些简单的转换规则来将 S 表达式转化为 SQL 语句的。也就是说,如果你已经懂得 SQL 了,你应该就能知道 EmacSQL 的低层运行机理。下面是一些例子,
(require 'emacsql)
;; Connect to the database, SQLite in this case:
(defvar db (emacsql-connect "~/office.db"))
;; Create a table with 3 columns:
(emacsql db [:create-table patients
([name (id integer :primary-key) (weight float)])])
;; Insert a few rows:
(emacsql db [:insert :into patients
:values (["Jeff" 1000 184.2] ["Susan" 1001 118.9])])
;; Query the database:
(emacsql db [:select [name id]
:from patients
:where (< weight 150.0)])
;; => (("Susan" 1001))
;; Queries can be templates, using $s1, $i2, etc. as parameters:
(emacsql db [:select [name id]
:from patients
:where (> weight $s1)]
100)
;; => (("Jeff" 1000) ("Susan" 1001))
一个查询就是一个由关键字,标识符,参数和数据组成的数组。这里参数的作用在于使得使用者无需在运行期动态地组建 S 表达式。
将 S 表达式编译成 SQL 语句的规则已经列在 EmacSQL 的文档中了,我这里就不再重复了。简单来说,lisp 关键字会转换成 SQL 关键字,要查询的记录信息(row-oriented information) 使用数组来表示,表达式使用 list 来表示,symbol 没有被引用的话则会转换成标识符。
[:select [name weight] :from patients :where (< weight 150.0)]
会被编译成:
SELECT name, weight FROM patients WHERE weight < 150.0;
另外,任何 可读的 lisp 值 都能存储到数据库的属性中。整数被映射出 INTEGER 型,小数被映射成 REAL 型,nil 被映射为 NULL,其他类型的值都以字面量的格式存储为 TEXT 类型。当然这种映射关系根据后端的不同而改变。
Parameters
以开头的被看成是参数。紧跟开头的symbol被看成是参数。紧跟的是参数的类型 — identifier (i), scalar (s), vector (v), schema (S) — 最后是参数的位置。
[:select [$i1] :from $i2 :where (< $i3 $s4)]
若接受三个 symbol 以及 1 个整数作为参数: name people age 21
,则会编译成:
SELECT name FROM people WHERE age < 21;
数组类型的参数引用的是带插入的行或者 IN 表达式中的集合。
[:insert-into people [name age] :values $v1]
若接受了一个由两行组成的 list 作为参数: (["Jim" 45] ["Jeff" 34])
,则会编译成
INSERT INTO people (name, age) VALUES ('"Jim"', 45), ('"Jeff"', 34);
还有这个例子:
[:select * :from tags :where (in tag $v1)]
若接受的参数为 [hiking camping biking]
,则会编译成
SELECT * FROM tags WHERE tag IN ('hiking', 'camping', 'biking');
当写这些 S 表达式时,记住可以使用命令 emacsql-show-last-sql
来在 minibuffer 中显示当前 S 表达式转换成的 SQL 语句是什么。
Schemas
表结构是用列表来表示的,该列表的第一个元素是由列名组成的数组(也就是说,记录信息(row-oriented information) 是以数组的形式来表示的)。list 中剩下的元素表示表格的约束条件。下面是摘自文档中的一些例子:
;; No constraints schema with four columns:
([name id building room])
;; Add some column constraints:
([(name :unique) (id integer :primary-key) building room])
;; Add some table constraints:
([(name :unique) (id integer :primary-key) building room]
(:unique [building room])
(:check (> id 0)))
我尝试过很多种语法来创建 EmacSQL 数据库,在这些语法中,表示表结构的方式一直没有改变过。表结构类似于程序中的类型定义,而行则是这些类型的是一个实例,因此使用类似 defstrcut
这样的结构来表示表结构是可行的。
这种结构表达式可以被 $S
类的参数所替代("S"表示 Schema).
(defconst foo-schema-people
'([(person-id integer :primary-key) name age]))
;; ...
(defun foo-init (db)
(emacsql db [:create-table $i1 $S2] 'people foo-schema-people))
Back-ends
目前为止我们所讨论的任何东西都只与 SQL 声明编译器有关。SQL 声明编译器与后端实现无关,这些后端被用于处理 SQL 声明编译产生的字符串。
SQLite Implementation Difficulties
一年多前,我用 Elisp 写过 一个 pastebin webapp 。我本想用 SQLite 作为后端来存储粘贴的内容,但是发现 SQLite 的命令行程序(sqlite3) 很难与 Emacs 进行整合。难点在于,除了"tcl"之外,所有的输出模式都很模糊。输出可能是以"csv"格式输出的。TEXT 属性值中可能包含换行符,这使得一条记录可能被分成了许多行。输出中可能包含类似 sqlite3 的提示符这样的内容,这样就无法搞清楚 sqlite3 是否已经将结果完全输出了。最终我认为 sqlite3 根本不适合与 Emacs 进行整合。
最近 alexbenjm 和 Andres Ramirez 开始讨论 在 Elfeed 中使用 SQLie 来作为后端。这个讨论给我以灵感,让我用另一种方式来处理 SQLite 输出的这种模糊性: 只使用 TEXT 来存储 Elisp 值的输出字面量! 只要将 print-escape-newlines
设置为非 nil,则 TEXT 值就不会被分隔为多行了,并且我还能使用 read
来从 sqlite3 的输出中还原原数据。所有的 sqlite3 的输出模式一下子清晰起来了。
然而,在解决了这个重大问题之后,我发现了一个更大的难题: GNU Readline。Linux package 仓库中的 sqlite3 程序几乎都在编译时开启了 Readline 支持了.开启 Readline 支持使得该工具更易于人使用,但对于 Emacs 来说却是个大难题。
First, sqlite3 the command shell is not up to the same standards as SQLite the database。在我使用 SQLite 的那么点时间里,我就发现了该程序的多个 BUG。其中一个是因为 sqlite3 这个程序并未很好地与 GNU Readline 整合在一起。sqlite3 中有一个 .echo
元命令可以设置是否回显输入的命令(该功能可能在某些情况下很有用,但对我来说无用)。该 BUG 产生的原因是该回显命令与 GNU Readline 的 eaho 是分开的,在激活 Readline 的情况下,若开启 .echo
则实际上会回显两次。若关闭 .echo
则回显一次。
Pseudo-terminals
在某些条件下,比如当通过管道而不是 PTY 进行通讯时,Readline 无法被激活。这个问题本应该被解决的,当 Readline 被禁用的后果是 sqlite3 大量的缓存输出内容。这使得无法与 sqlite3 进行正常的交互。更糟糕的是,在 Windows 平台上 错误信息也可能被缓存 ,这样一来 sqlite3 的出错信息都可能长时间不显示(这是 sqlite3 的又一个 bug).
除了 Readline 无法正常输出的问题之外,还有一个问题是 Readline 无法接收到控制字符。ASCII 表中头 32 个字符被认为是控制字符。不处于 raw 模式下的伪终端(PTY) 会立即对输入的控制字符做出反应。There’s no escaping them.
Emacs 默认通过 PTY 与其子进程进行通讯(这可能是早期设计上的一个错误),这就限制住了可以被发送的数据范围。你可以自己试一下。执行 M-x sql-sqlite
(该命令是 Emacs 内置的) 然后试着发送任意包含 0x1C 字符的字符串。你可以通过按下 C-q C-\
来输入这个特殊字符,但发送这个字符会使得子进程挂掉。
有两种方法解决这个特殊字符的问题。一种方法是使用管道进行通讯(方法是设置 process-connection-type
为 t),因为管道并不会响应控制字符。然而由于上面提到的缓存问题,因此这种方法不适用于 sqlite3.
另一种解决方法是将 PTY 置于 raw 模式下。不幸的是,Emacs 中并没有函数来实现这个功能,你不得不通过调用 stty 程序来完成这个动作。当然,由于需要在同一个 PTY 上运行 stty ,因此我们需要用到 start-process-shell-command
命令。
(start-process-shell-command name buffer "stty raw && <your command>")
Windows 平台既没有 stty 命令,也没有 PTY(或任何类似 PTY 的东西),因此在运行进程前你需要先检查一下所处的操作系统。然而即使是这种方法也不适用于 sqlite3,因为 Readline 本身就会响应这些控制字符,而且没有办法禁止掉。
有一个叫做 esqlite 的 package,也是 SQLite 的前端。它就是基于 sqlite3 命令的,因此深受这些问题的侵扰。
A Custom SQLite Binary
由于 sqlite3 如此不可靠,我设计了自己的协议并开发了相关的外置程序。该程序只是一小段 C 代码,它接受一个 SQL 字符串然后将查询结果转换为 S 表达式的格式返回。借助这段 C 程序,我不再需要强制存储 lisp 值的字面量了,但我依然保留了这一范式。因为这样做可以简化这段 C 程序的实现,更重要的是,我可以完全依赖 Emacs 的 reader
来解析查询结果。这使得 Emacs 能够与子进程尽可能快地进行通讯。毕竟 reader
要比任何 Elisp 程序更快。
我之前提到过的,当具备条件的情况下,安装程序会直接编译这段 C 程序,否则会从我的服务器上直接下载预编译好的程序(当然,只支持常见的那几个平台)。也就是说,不管你用的什么平台,EmacSQL 至少都有一种可用的后端。
Other Back-ends
EmacSQL 同时支持 PostgreSQL 与 MySQL,当然,前提是已经安装了响应的客户端程序(psql/mysql)。两者处理起来都比 sqlite3 要好的多,通过调用 stty
设置 PTY 为 raw mode,无需任何其他的帮助就能很好的解析两者的输出。两种后端都通过了所有的单元测试,所以,技术上来说,它们都能正常的工具。
要用它们来实现本文一开始的那些例子,需要先 require emacsql-psql
或 emacsql-mysql
,然后替换 emacsql-connect
为 emacsql-psql
或 emacsql-mysql
的构造函数(参数也需要作响应改变)。所有这三个构造函数都返回一个 emacsql-connection 对象,并且共用同一个 API.
EmacSQL 目前只为这几个数据库提供了统一的接口。所有操作数据库连接的函数都是泛型函数(EIEIO),这样,改变后端只会影响到程序的 SQL 声明而已。例如,if you use SQLite-ism (dynamic typing) it won’t translate to either of the other databases should they be swapped in.
以后我会再写写关于数据库连接的 API 及其实现方式。除了处理 PTY 这部分内容外,其实还蛮简单的。比如 MySQL 的实现只有区区 80 行代码而已。
EmacSQL’s Future
我希望 EmacSQL 能成为可供其他 package 依赖的可信任的数据库解决方案。截止到目前未知,已经有两个 package 使用了 EmacSQL: pastebin demo 和 Elfeed,我希望有更多人使用这个 package 而不是自己去 hacker 数据库。
我已经重新创建了一个分支用于使用 EmacSQL 是重新实现其数据库操作。总有一天,我会使用它作为 Elfeed 与数据库交互的主要方式。EmacSQL 所使用的 SQLite 开启了 full-text search engine,这使得 Elfeed 的搜索 API 可以即强大又快速。目前来看,主要的问题是 Elfeed 的数据库 API 与 ACID 数据库事务不那么兼容 — 这是我的短视所造成的(shortsightedness on my part)!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: INSIDE_EMACS 变量
下一篇: cd 到远程主机
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论