「这是我参与11月更文挑战的第 13 天,活动详情查看:2021最后一次更文挑战」
构建项目
你需要做的第一件事是在你的Cargo.toml文件中添加该依赖项:
[dependencies]
rustyline = "9.0.0"
rustyline-derive = "0.5.0"
thiserror = "1.0.24"
sqlparser = "0.12.0"
serde = { version = "1.0.123", features = ["derive", "rc"] }
structopt = "0.3.25"
prettytable-rs = "0.8.0"
log = "0.4.14"
env_logger = "0.9.0"
复制代码
上面我也提到我要利用 rustyline
的一些特性,这就是为什么我们有 rustyline-derive
依赖的原因,这个 crate 实际上只是一些过程宏,帮助我们实现必要的特性,以便用rustyline向我们的REPL程序添加验证、提示和完成等东西。
structopt crate是一个struct解析器,也相当成熟,它解决了我们为创建自己的CLI解析器而必须编写的大量模板代码,因为内含有 clap crate(命令行解析器)。现在,它实际上没有做什么,只是在我们输入 rust-sqlite --help
时显示一些帮助信息。
另外两个依赖项:log,env_logger 是为了使整个应用程序的日志记录更容易,比如说,为我们管理不同的日志级别。
main.rs
为了保持代码的简洁和可读性,我会尽可能地保持我的主文件的简洁。在 main.rs
你可能已经注意到了,我在第2行声明了一个 mod repl;
。这是因为我把与我们的REPL程序部分有关的大部分业务逻辑移到了一个不同的模块。这样,我们的main.rs可以保持干净,易于阅读。
所以,我导入了一个结构和一个函数 (from repl module)
use repl::{REPLHelper, get_config};
复制代码
rustyline
的 helper struct
负责实现我们希望在REPL程序上实现的所有行为。
例如,我们正在使用 Validator trait
,它可以在用户输入时实时验证输入。我们还使用了 Hinter trait
,负责在用户输入命令时提供提示。
所有与rustyline REPL的实现和行为相关的代码都是一个单独的模块。同时我认为这些辅助模块会给用户在使用程序带来更好的感觉,另外 sqlite3
也提供了这些,我们既然是复刻也要提供。
交互分析
上次我们所做的是一个简单的CLI应用程序,它将用于外部命令和一些附属功能,还有一个简单的REPL,它还需要采取一个简单的命令来优雅地退出应用程序。
首先,我们要对输入进行解析,以便能够区分输入是 MetaCommand
还是 SQLCommand
。MetaCommand以 .
开始,并采取直接行动,如 .open/.help/.exit
。而SQLCommand是啥,这个你应该知道的。
我们要做的第二步是能够解析每个命令类型并采取适当的行动。
现在,我们开始往数据库方面靠近。REPL会接收命令,然后区分不同的SQL语句,并将它们的组成部分分解成若干部分,准备执行。因此,下一次我们可以把重点放在获取解析后的SQL语句并执行它。
SQLite的前端部分解析SQL语句,对其进行优化(优化器参与),正如在前一篇文章中提到的,内部会生成等价的内部表示形态,称为:字节码。
然后,字节码被传递给后端的虚拟机,由它来执行。而对于前端解析SQL → 字节码,整个步骤也会分成几步:
- 词法分析: Tokenizer
- 语法分析: Parser
- 生成最终字节码: Code Generator
像这样把逻辑分成几步,有几个好处:
- 降低了每个部分的复杂性(例如,虚拟机不担心语法错误,因为在前面的词法分析已经过滤了)。
- 允许一次性编译普通查询,并缓存字节码以提高性能。