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