昨天我release了第一版的 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语句. 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类型. 当然这种映射关系根据后端的不同而改变.
以$开头的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语句是什么.
表结构是用列表来表示的,该列表的第一个元素是由列名组成的数组(也就是说,记录信息(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))
目前为止我们所讨论的任何东西都只与SQL声明编译器有关. SQL声明编译器与后端实现无关,这些后端被用于处理SQL声明编译产生的字符串.
一年多前,我用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
则回显一次.
在某些条件下,比如当通过管道而不是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命令的,因此深受这些问题的侵扰.
由于sqlite3如此不可靠,我设计了自己的协议并开发了相关的外置程序. 该程序只是一小段C代码,它接受一个SQL字符串然后将查询结果转换为S表达式的格式返回. 借助这段C程序,我不再需要强制存储lisp值的字面量了,但我依然保留了这一范式. 因为这样做可以简化这段C程序的实现, 更重要的是,我可以完全依赖Emacs的 reader
来解析查询结果. 这使得Emacs能够与子进程尽可能快地进行通讯. 毕竟 reader
要比任何Elisp程序更快.
我之前提到过的,当具备条件的情况下,安装程序会直接编译这段C程序,否则会从我的服务器上直接下载预编译好的程序(当然,只支持常见的那几个平台). 也就是说,不管你用的什么平台,EmacSQL至少都有一种可用的后端.
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能成为可供其他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)!