编程 Turso 深度拆解:当 SQLite 的 Rust 重写版决定「顺便说一口 Postgres」——从 VDBE 单核多前端到 BEGIN CONCURRENT,一个自称「数据库界 LLVM」的引擎如何重构嵌入式数据库的终极形态

2026-08-08 14:48:18 +0800 CST views 8

Turso 深度拆解:当 SQLite 的 Rust 重写版决定「顺便说一口 Postgres」——从 VDBE 单核多前端到 BEGIN CONCURRENT,一个自称「数据库界 LLVM」的引擎如何重构嵌入式数据库的终极形态

两年前它叫 Limbo,是「用 Rust 重写 SQLite」的又一次玩票;今天它叫 Turso,GitHub 仓库简介写着一句很狂的话:

A SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.

2026-08-06,Turso 发布了 v0.8.0-pre.3。距离 1.0 还有一段路,但这个项目已经不是当初那个「Rust 版 SQLite」了。它做了一件在数据库圈里非常少见的事:把 SQLite 的 VDBE 字节码虚拟机抽象成通用后端,然后在上面挂了第二个前端——PostgreSQL。同一个进程内引擎,既能吃 SQLite 方言、复用 SQLite 文件格式和 C API,也能开一个 pgwire v3 端口,让 psql 直接连上来。

这篇文章不复述发布公告。我关心的是四件工程上真正硬的东西:

  1. 「一个 VM、多个 SQL 前端」这个类比 LLVM 的架构,到底是营销话术还是真的成立?
  2. Postgres 兼容层是怎么焊上去的,它的失败模式在哪里?
  3. BEGIN CONCURRENT + MVCC 怎么突破 SQLite 单写者的天花板,代价是什么?
  4. Rust 重写真正的红利是不是「内存安全」——我的答案是不是,而是可失败的内存分配异步 I/O

以及最后:什么时候可以上生产,什么时候绝对不行。


一、背景:这已经是 Turso 团队第二次动 SQLite 了

要理解 Turso 现在的形态,得先理解他们踩过的坑。

第一次是 fork。 团队在 2023 年 fork 了 SQLite,做出 libSQL,加了远程复制、向量搜索这些现代特性,拿到一万多 star。fork 的好处很直接:可以持续从上游 merge,新特性白拿。

但 fork 有两个致命问题:

  • SQLite 的测试套件是专有的。 SQLite 以「测试覆盖率地狱级」闻名,但那套 TH3 测试并不开源。你 fork 了代码,却没 fork 到信心。改一行 B-tree 代码,你不知道自己破坏了什么。
  • C 语言的内存安全上限摆在那里。 想大改架构(比如引入 MVCC、异步 I/O),在一份三十年历史的 C 代码库里做,风险是指数级的。

于是有了第二次:完全重写。用 Rust,从零实现,保持 SQLite 兼容。这就是 Limbo,后来改名 Turso Database。

这里有个容易被忽略的判断:重写的动机不是「C 不安全」,而是「我不敢改」。这两件事是不一样的。前者是意识形态,后者是工程现实。当你没有上游那套测试套件、又必须做架构级改动时,重写反而是风险更低的路。

到今天,Turso 的功能清单已经相当长了(以下均来自仓库 README 与 manual,非营销材料):

已稳定的部分:

  • SQLite 兼容:SQL 方言、文件格式、C API
  • BEGIN CONCURRENT:基于 MVCC 的并发写
  • CDC(变更数据捕获)
  • 多语言绑定:Rust / Go / JavaScript / Java / .NET / Python / WebAssembly
  • Linux 上的 io_uring 异步 I/O
  • 跨平台:Linux、macOS、Windows、浏览器(WASM)
  • 向量支持:精确搜索与向量运算

明确标注实验性的部分:

  • Postgres 兼容:SQL 方言 + wire protocol
  • 静态加密(encryption at rest)
  • 基于 DBSP 的增量计算:增量视图维护、查询订阅
  • 基于 tantivy 的全文检索
  • 通过 .tshm sidecar 实现的多进程 WAL 协调

看这份清单,你会发现一件事:它早就不在「复刻 SQLite」的赛道上了。SQLite 不会给你 MVCC,不会给你 DBSP 增量视图,更不会给你 Postgres wire protocol。


二、核心概念:把 VDBE 当成数据库世界的 LLVM IR

2.1 SQLite 架构的那个「隐藏资产」

SQLite 的执行模型非常经典:

SQL 文本
  → Parser(生成 AST)
  → Code Generator(生成 VDBE 字节码)
  → VDBE(虚拟机,逐条执行 opcode)
  → B-tree / Pager / OS 接口

绝大多数人把 VDBE 当成一个实现细节。但换个视角看:VDBE 就是数据库领域的中间表示(IR)

它有 IR 该有的一切特征:

  • 指令集是稳定的、与前端语法解耦的(OpenReadSeekGEColumnResultRowNext...)
  • 优化可以在生成 IR 之前(逻辑优化)或之后(peephole)做
  • 执行引擎只认 IR,不认 SQL

Turso 的洞见就是这一句:既然 VDBE 是 IR,那前端为什么只能有一个?

2.2 LLVM 类比到底成不成立

LLVM 的模型是:

Clang (C/C++) ─┐
rustc      ────┼→ LLVM IR → 优化 Pass → x86 / ARM / RISC-V / WASM
swiftc     ────┘

Turso 的模型是:

SQLite 前端  ─┐
Postgres 前端 ┼→ Turso AST → VDBE 字节码 → VDBE 执行引擎 → Pager / MVCC / io_uring
(更多前端) ─┘

这个类比在结构上是成立的,而且比大多数「XX 界的 LLVM」要靠谱得多,因为它满足关键条件:IR 是先于这个想法存在的,而不是为了这个想法硬造的。VDBE 已经被 SQLite 验证了二十多年。

官方还有个很皮的证据:他们做了个 turso-vdbe-doom-example在 VDBE 上跑 Doom。这当然是玩梗,但它证明的东西是实在的——这个虚拟机的指令集通用到能表达任意计算,而不只是关系代数。

2.3 但类比也有边界

我必须泼一盆冷水。LLVM 的成功建立在一个前提上:C、C++、Rust、Swift 的运行时语义高度重叠(都是指针、整数、栈、调用约定)。

数据库前端不是这样。Postgres 和 SQLite 的差异不在语法层,而在语义层和类型系统层

维度SQLitePostgreSQL
类型系统动态类型 + 亲和性(affinity)强静态类型,隐式转换规则严格
并发模型单写者 + WALMVCC + 多种隔离级别
NULL 与空串区分,但比较规则宽松严格三值逻辑
事务隔离实质上是串行化RC / RR / Serializable 可选
扩展体系虚拟表 / loadable extension完整的 extension + FDW + 后台 worker

把 Postgres 的 SQL 压到一个为 SQLite 设计的 VDBE 上,必然会有语义损耗。问题不是「有没有损耗」,而是「损耗时会不会报错」。这直接引出下一节。


三、架构拆解:Postgres 前端是怎么焊上去的

Turso 的 postgres/ 目录结构非常清晰,四个组件:

组件职责
postgres/parserpg_query(libpg_query,真正的 PostgreSQL 语法)解析 SQL,然后 translate 成 Turso AST
postgres/frontendpg_catalog 模拟、COPY、schema、session 处理
postgres/serverPostgreSQL wire protocol v3 服务端,基于 pgwire
postgres/clitursopg,一个类 psql 的 REPL,也能直接托管 server

3.1 最关键的设计决策:用 libpg_query 而不是自己写 parser

这一步是对的。libpg_query 是从 PostgreSQL 源码里抽出来的真实语法解析器,意味着 Turso 在语法层面对 Postgres 是 100% 兼容的——任何 psql 能解析的语句,Turso 也能解析。

但也正因为这样,产生了整个兼容层最危险的特性。官方文档自己写得很直白:

Because the parser accepts the full PostgreSQL grammar, some clauses parse and run but their semantics are silently dropped, producing wrong results or lost information without any error.

翻译成人话:语法全收,语义选择性执行,丢了不报错。

3.2 「静默丢弃」清单——这是你必须记住的部分

我把 postgres/COMPAT.md 里所有标注为静默忽略或降级的行为整理出来,这是全文最有实用价值的一张表:

语句实际行为后果
DELETE ... USINGUSING 子句被静默丢弃删除范围可能远超预期
TRUNCATE a, b, c降级为对第一张表DELETECASCADE / RESTART IDENTITY 丢弃b、c 表数据还在,序列没重置
CREATE TEMP TABLETEMP 被静默忽略临时表变成了持久表
CREATE TEMP VIEW同上同上
COMMENT ON接受但丢弃,不写入 pg_descriptionobj_description() 永远返回 NULL
BEGIN ISOLATION LEVEL ...隔离级别与 READ ONLY/WRITE接受但忽略你以为设了 Serializable,其实没有
SET / SHOW转成 PRAGMA,无 GUC 概念SHOW search_path 返回空
ILIKE / ~* / !~*转成 REGEXP,大小写不敏感变体被当作敏感处理查询结果直接错
interval / xml / tsvector / tsquery / bit / 几何类型降级为 TEXT类型运算全废
money降级为 REAL精度问题
CREATE TABLE AS ... WITH NO DATA降级为 LIMIT 0被覆盖的 LIMIT 里的错误不上报

我的判断:这是兼容层最糟糕的失败模式。 报错是好事——报错意味着你在开发期就知道了。静默降级意味着你在生产环境的某个凌晨三点,看着一张本该只删 10 行却删了 100 万行的表发呆。

如果你真要试 Turso 的 Postgres 前端,第一件事不是跑业务,而是写兼容性探针

3.3 实战:给 Postgres 前端写一个兼容性探针

思路很简单:对每一条你实际会用到的 SQL 模式,构造一个「如果语义被丢弃,结果必然不同」的断言。

#!/usr/bin/env python3
"""
turso_pg_probe.py — Turso Postgres 前端语义探针
用法: python3 turso_pg_probe.py "postgresql://turso@127.0.0.1:5432/main"

原理: 不测「能不能跑」,测「跑出来的语义对不对」。
      静默降级的特征就是: 不抛异常, 但结果和 PG 不一致。
"""
import sys
import psycopg


PROBES = []


def probe(name, critical=False):
    def deco(fn):
        PROBES.append((name, fn, critical))
        return fn
    return deco


@probe("DELETE ... USING", critical=True)
def p_delete_using(cur):
    cur.execute("DROP TABLE IF EXISTS probe_a; DROP TABLE IF EXISTS probe_b;")
    cur.execute("CREATE TABLE probe_a(id INT, v INT);")
    cur.execute("CREATE TABLE probe_b(id INT);")
    cur.execute("INSERT INTO probe_a VALUES (1,1),(2,2),(3,3);")
    cur.execute("INSERT INTO probe_b VALUES (2);")
    # PG 语义: 只删 id=2 这一行, 剩 2 行
    cur.execute("DELETE FROM probe_a USING probe_b WHERE probe_a.id = probe_b.id;")
    cur.execute("SELECT count(*) FROM probe_a;")
    left = cur.fetchone()[0]
    assert left == 2, f"USING 被丢弃: 期望剩 2 行, 实际剩 {left} 行"


@probe("TRUNCATE 多表", critical=True)
def p_truncate_multi(cur):
    cur.execute("DROP TABLE IF EXISTS t1; DROP TABLE IF EXISTS t2;")
    cur.execute("CREATE TABLE t1(x INT); CREATE TABLE t2(x INT);")
    cur.execute("INSERT INTO t1 VALUES (1); INSERT INTO t2 VALUES (1);")
    cur.execute("TRUNCATE t1, t2;")
    cur.execute("SELECT (SELECT count(*) FROM t1) + (SELECT count(*) FROM t2);")
    total = cur.fetchone()[0]
    assert total == 0, f"TRUNCATE 只作用于第一张表: 残留 {total} 行"


@probe("ILIKE 大小写不敏感", critical=True)
def p_ilike(cur):
    cur.execute("DROP TABLE IF EXISTS t_ilike;")
    cur.execute("CREATE TABLE t_ilike(s TEXT);")
    cur.execute("INSERT INTO t_ilike VALUES ('Hello');")
    cur.execute("SELECT count(*) FROM t_ilike WHERE s ILIKE 'hello';")
    n = cur.fetchone()[0]
    assert n == 1, "ILIKE 退化成大小写敏感匹配"


@probe("TEMP TABLE 是否真临时")
def p_temp(cur):
    cur.execute("CREATE TEMP TABLE probe_temp(x INT);")
    cur.execute("SELECT count(*) FROM pg_class WHERE relname = 'probe_temp';")
    # 若 TEMP 被忽略, 它会以普通表身份出现在 catalog 里
    n = cur.fetchone()[0]
    assert n == 0, "TEMP 被静默忽略, 临时表变成了持久表"


@probe("事务隔离级别是否生效")
def p_isolation(cur):
    cur.execute("BEGIN ISOLATION LEVEL SERIALIZABLE;")
    cur.execute("SHOW transaction_isolation;")
    row = cur.fetchone()
    cur.execute("ROLLBACK;")
    assert row and "serializable" in str(row[0]).lower(), \
        f"隔离级别被忽略, SHOW 返回 {row}"


def main(dsn):
    ok = fail = 0
    with psycopg.connect(dsn, autocommit=True) as conn:
        for name, fn, critical in PROBES:
            with conn.cursor() as cur:
                try:
                    fn(cur)
                    print(f"  [PASS] {name}")
                    ok += 1
                except AssertionError as e:
                    flag = "CRIT" if critical else "WARN"
                    print(f"  [{flag}] {name}: {e}")
                    fail += 1
                except Exception as e:
                    print(f"  [ERR ] {name}: {type(e).__name__}: {e}")
                    fail += 1
    print(f"\n通过 {ok} / 失败 {fail}")
    return 1 if fail else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1]))

跑一遍,你就知道自己的 ORM 生成的 SQL 会在哪里翻车。这个成本远低于线上事故的成本。

3.4 类型映射:serial 的处理很聪明

有一处设计我要单独夸一下。Postgres 的 serial 系列类型,Turso 是这样映射的:

-- 你写的
CREATE TABLE users (id serial PRIMARY KEY, name text);

-- Turso 实际展开成
CREATE TABLE users (
    id INTEGER NOT NULL DEFAULT nextval('users_id_seq'),
    name TEXT,
    PRIMARY KEY (id)
) STRICT;
-- 并隐式创建 users_id_seq

注意两点:

  1. 它真的实现了 CREATE SEQUENCE / nextval / currval / setval,包括 STARTINCREMENTMIN/MAXVALUECYCLE,以及 pg_sequences 视图。CHANGELOG 里对应的提交是 feat: add PostgreSQL-style sequences and MVCC-safe AUTOINCREMENT——MVCC-safe 这个词是重点,序列在并发事务下的行为是被专门处理过的。
  2. 所有通过 PG 前端建的表都是 STRICT。这很关键:Postgres 是强类型的,如果建出 SQLite 那种动态类型表,INSERT INTO t(int_col) VALUES ('abc') 会静默成功,语义就彻底崩了。用 STRICT 是唯一正确的选择。

pg_catalog 的模拟也做得比我预期的全:pg_classpg_namespacepg_attributepg_type(含 array 和 enum)、pg_indexpg_constraintpg_attrdefpg_tablespg_sequencespg_databasepg_rolespg_procpg_am 都是活的(反映真实 schema),另有一批空表占位。这意味着很多依赖 catalog 内省的工具(DBeaver、部分 ORM 的 migration 检测)能跑起来。

CREATE SCHEMA 的实现也很有意思:schema 就是 ATTACH 的数据库public 特殊处理。一个嵌入式引擎能用 ATTACH 机制映射出 namespace 概念,这个复用很漂亮。


四、MVCC 与 BEGIN CONCURRENT:突破单写者天花板

4.1 SQLite 的天花板在哪

SQLite 在 WAL 模式下的并发模型是:多读者 + 单写者。任何时刻只能有一个写事务。写多的场景下,SQLITE_BUSY 是你的老朋友。

这不是 bug,是设计。SQLite 的目标场景是「一个应用进程用一个文件」,单写者换来的是极简的实现和地狱级的可靠性。

但 2026 年的嵌入式场景变了:一个 Node.js 服务几十个并发请求、一个 AI Agent 十几个子任务并行写记忆库、边缘节点上跑多租户 —— 单写者就是瓶颈。

4.2 Turso 的三种事务模型

模式加锁时机并发能力冲突表现
DEFERRED(默认)首次读时开读事务,首次写时升级为写事务多读单写写升级失败 → SQLITE_BUSY
IMMEDIATE / EXCLUSIVEBEGIN 时立刻抢写锁多读单写BEGIN 就可能 BUSY
CONCURRENT(需 MVCC)从不加锁多读多写提交时冲突检测 → SQLITE_BUSY

EXCLUSIVE 在 Turso 里是 IMMEDIATE 的别名,这一点和 SQLite 的 WAL 模式行为一致。

4.3 CONCURRENT 事务的完整生命周期

这是文档里写得最扎实的一段,我拆开讲。

开始时(BEGIN CONCURRENT TRANSACTION):

  1. 分配唯一事务 ID
  2. 从逻辑时钟记录 begin timestamp
  3. 创建空的 read setwrite set
  4. 不获取任何锁

运行期间(快照隔离):

  • 能看到 begin timestamp 之前所有已提交的数据
  • 看不到在自己开始之后提交的其他事务的写入
  • 同一事务内可重复读
  • 多个 concurrent 事务可以同时读写,互不阻塞
  • 所有读到的行进 read set,所有写的行进 write set

提交时(三步):

  1. 独占事务检查:如果此刻有活跃的 exclusive 事务(BEGIN IMMEDIATE,或者升级成写事务的 BEGIN DEFERRED),concurrent 事务无法提交,直接 SQLITE_BUSY
  2. 写写冲突检测:遍历 write set 里每一行,检查是否满足以下任一条件:
    • 该行正被另一个活跃事务修改
    • 该行被某个在本事务 begin timestamp 之后提交的事务修改过
  3. 提交或中止:无冲突则提交,write set 里所有 row version 的 begin timestamp 更新为本事务的 commit timestamp;有冲突则 SQLITE_BUSY,应用层必须回滚重试。

有一个很微妙的设计点:exclusive 事务阻塞 concurrent 事务的提交,但不阻塞它的读写。也就是说 concurrent 事务在独占事务运行期间可以继续干活,只是不能落地。这个设计让 schema 变更这类必须独占的操作能真正独占,同时又不让并发事务白白空转。

官方给的最佳实践很明确:MVCC 模式下,所有写事务都用 BEGIN CONCURRENT;只有真正需要排他写权限时才用 IMMEDIATE / DEFERRED

4.4 隔离级别的真相:是快照隔离,不是可串行化

文档写得很诚实:CONCURRENT 提供的是 snapshot isolation

这意味着 write skew(写偏斜)异常依然存在。经典例子:

-- 业务规则: 任何时刻至少要有一位医生在值班
-- 初始: Alice 和 Bob 都在值班

-- 事务 T1 (Alice 请假)
BEGIN CONCURRENT;
SELECT count(*) FROM doctors WHERE on_call = 1;  -- 读到 2, OK
UPDATE doctors SET on_call = 0 WHERE name = 'Alice';
COMMIT;  -- write set = {Alice 行}

-- 事务 T2 (Bob 请假), 与 T1 并发
BEGIN CONCURRENT;
SELECT count(*) FROM doctors WHERE on_call = 1;  -- 也读到 2, OK
UPDATE doctors SET on_call = 0 WHERE name = 'Bob';
COMMIT;  -- write set = {Bob 行}

两个事务的 write set 完全不相交(一个改 Alice 行,一个改 Bob 行),写写冲突检测不会触发,两个都提交成功。结果:没有医生值班,业务约束被破坏。

这不是 Turso 的 bug,这是快照隔离的定义性缺陷。 但你必须知道,因为很多人下意识把 MVCC 等同于「安全」。

规避方式有三种:

-- 方案 1: 把约束物化成一行, 强制制造写冲突
UPDATE oncall_counter SET n = n - 1 WHERE ward = 'ICU';
-- 两个事务都会写这一行 -> 必然冲突 -> 一个被拒

-- 方案 2: 该业务走 BEGIN IMMEDIATE, 牺牲并发换正确性

-- 方案 3: 用唯一约束 / CHECK 让数据库替你兜底

4.5 实战:正确的 CONCURRENT 重试封装

SQLITE_BUSY 在 MVCC 下不是异常,是正常控制流。乐观并发的前提就是「冲突了就重试」。但重试有讲究——朴素的立即重试会造成惊群,把冲突率推到更高。

Rust 版:

use rand::Rng;
use std::time::Duration;
use turso::{Connection, Error as TursoError};

/// 判断错误是否为可重试的 MVCC 冲突
fn is_retryable(e: &TursoError) -> bool {
    // Turso 把写写冲突映射为 SQLITE_BUSY
    let s = e.to_string();
    s.contains("SQLITE_BUSY") || s.contains("database is locked")
}

/// 带指数退避 + 抖动的 CONCURRENT 事务执行器
///
/// 注意: 每次重试必须重新执行整个闭包(包括读),
/// 因为快照已经过期, 复用旧读结果会导致逻辑错误。
pub async fn with_concurrent_txn<F, Fut, T>(
    conn: &Connection,
    max_retries: u32,
    mut body: F,
) -> anyhow::Result<T>
where
    F: FnMut(&Connection) -> Fut,
    Fut: std::future::Future<Output = anyhow::Result<T>>,
{
    let mut attempt = 0u32;
    loop {
        conn.execute("BEGIN CONCURRENT", ()).await?;

        match body(conn).await {
            Ok(val) => match conn.execute("COMMIT", ()).await {
                Ok(_) => return Ok(val),
                Err(e) if is_retryable(&e) && attempt < max_retries => {
                    let _ = conn.execute("ROLLBACK", ()).await;
                    backoff(attempt).await;
                    attempt += 1;
                    continue;
                }
                Err(e) => {
                    let _ = conn.execute("ROLLBACK", ()).await;
                    return Err(e.into());
                }
            },
            Err(e) => {
                let _ = conn.execute("ROLLBACK", ()).await;
                return Err(e);
            }
        }
    }
}

async fn backoff(attempt: u32) {
    // base = 2^attempt * 1ms, 上限 200ms, 叠加 0~50% 抖动
    let base = (1u64 << attempt.min(8)).min(200);
    let jitter = rand::thread_rng().gen_range(0..=(base / 2).max(1));
    tokio::time::sleep(Duration::from_millis(base + jitter)).await;
}

Python 版(更贴近日常):

import random
import time
import turso


class ConflictRetryExhausted(Exception):
    pass


def concurrent_txn(conn, fn, max_retries=8):
    """
    在 BEGIN CONCURRENT 下执行 fn(cur), 冲突自动重试。

    关键约束:
      1. fn 必须是幂等的 —— 它会被整体重放
      2. fn 内部不要做外部副作用(发邮件/调 API), 那些要挪到提交之后
      3. 不要在 fn 里缓存上一轮读到的值
    """
    for attempt in range(max_retries + 1):
        cur = conn.cursor()
        try:
            cur.execute("BEGIN CONCURRENT")
            result = fn(cur)
            cur.execute("COMMIT")
            return result
        except turso.OperationalError as e:
            msg = str(e).lower()
            retryable = "busy" in msg or "locked" in msg
            try:
                cur.execute("ROLLBACK")
            except Exception:
                pass
            if not retryable or attempt == max_retries:
                raise
            # 指数退避 + 全抖动 (AWS full jitter)
            cap = min(0.2, 0.001 * (2 ** attempt))
            time.sleep(random.uniform(0, cap))
    raise ConflictRetryExhausted(f"{max_retries} 次重试后仍冲突")


# 使用示例: 并发扣库存
def deduct_stock(sku, qty):
    def body(cur):
        cur.execute("SELECT stock FROM inventory WHERE sku = ?", (sku,))
        row = cur.fetchone()
        if row is None:
            raise ValueError(f"未知 SKU: {sku}")
        if row[0] < qty:
            raise ValueError("库存不足")
        cur.execute(
            "UPDATE inventory SET stock = stock - ? WHERE sku = ?", (qty, sku)
        )
        return row[0] - qty
    return concurrent_txn(conn, body)

三条踩坑经验:

  1. 一个连接只能有一个活跃事务。 文档明确写了:需要并发(包括 BEGIN CONCURRENT)时,必须用不同的连接,而不是在同一连接上并行执行语句。所有在同一连接上 prepare 的语句都属于同一个事务上下文。用连接池,别偷懒。
  2. 重试必须重放整个事务体,包括读。 快照过期后,你上一轮读到的值就是脏的。见过有人只重放 UPDATE 不重放 SELECT,结果扣出负库存。
  3. 别在事务体里做外部副作用。 事务会被重放 N 次,发邮件也会发 N 次。

4.6 MVCC 现在的真实状态:别急着上生产

manual 里的 MVCC 限制清单,我原样搬过来,每一条都很重。

  • 索引不能创建,带索引的数据库不能用
  • 首次访问时把全部数据从磁盘急切加载到内存——大库启动极慢且吃内存
  • 只支持 PRAGMA wal_checkpoint(TRUNCATE),而且同时阻塞读者和写者
  • 很多特性可能不工作、工作不正确,或者直接 panic
  • 查询可能返回错误结果
  • 用 MVCC 写过的库,不 checkpoint 就用非 MVCC 模式打开,改动不可见

第一条和第二条基本判了死刑:没有索引 + 全量入内存 = 只能玩玩

另外 CHANGELOG 里有一条值得注意:core: reject co-enablement of MVCC and multiprocess WAL——MVCC 和多进程 WAL 现在是互斥的,不能同时开。

所以我的判断是:BEGIN CONCURRENT 的架构设计是对的,实现完成度还差得远。 现在应该做的是在测试环境里跑起来验证你的重试逻辑,等索引支持落地再谈迁移。


五、Rust 重写真正的红利:不是内存安全,是「可失败的分配」和异步 I/O

这一节是我认为整个项目里最被低估的部分。

5.1 异步 I/O:让 sqlite3_step 可以「先返回」

SQLite 的 API 是彻底同步的。sqlite3_step() 调用下去,如果页不在缓存里,就得等磁盘。在单线程事件循环(Node.js、Python asyncio、Tokio)里,这就是一次硬阻塞。

Turso 在 Linux 上用 io_uring 做异步 I/O,并且扩展了 sqlite3_step 的语义:数据没准备好时可以立刻返回,让调用方去干别的。

CHANGELOG 里能看到这条主线推进得非常猛:

  • Begin to remove all blocking IO from core
  • core: More blocking I/O removal
  • Remove blocking IO from mvcc bootstrap and recovery paths
  • Eliminate blocking I/O in page cache spill path
  • Make durable storage log completion awaitable
  • Step result yield
  • add yielding IO back-end for turso-stress to test async re-entrancy
  • core/io: add runtime-registrable Rust IO backend

最后那条尤其重要:可以在运行时注册自定义 I/O 后端。这意味着你能把 Turso 的 I/O 接到自己的 runtime 上——不管是 Tokio、glommio 还是某个自研的 thread-per-core 框架。

Rust 侧用起来就是原生 async:

use turso::Builder;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let db = Builder::new_local("app.db").build().await?;
    let conn = db.connect()?;

    conn.execute(
        "CREATE TABLE IF NOT EXISTS events (
            id    INTEGER PRIMARY KEY,
            ts    INTEGER NOT NULL,
            kind  TEXT NOT NULL,
            body  TEXT
        )",
        (),
    ).await?;

    // 整个 IO 路径都是 await 的, 不会卡住 executor
    let mut rows = conn
        .query("SELECT id, kind FROM events WHERE ts > ? ORDER BY ts LIMIT 100", (1_700_000_000i64,))
        .await?;

    while let Some(row) = rows.next().await? {
        let id: i64 = row.get(0)?;
        let kind: String = row.get(1)?;
        println!("{id} {kind}");
    }
    Ok(())
}

对比一下 rusqlite:那是同步 API,在 async 场景里你要么 spawn_blocking(线程池开销 + 上下文切换),要么直接阻塞 executor(灾难)。Turso 是从内核 I/O 一路 async 上来的,这是重写才能拿到的红利,fork 拿不到。

5.2 可失败的内存分配:这才是 Rust 重写的杀手锏

翻 CHANGELOG,你会看到一整条极其密集的提交线:

  • core/alloc: add stable push_within_capacity helper
  • Add allocator API for fallible allocation
  • core/alloc: Add allocation site annotations and out-of-memory fault injector
  • core/skiplist: use TursoAllocator for node allocation and add fallible inserts
  • mvcc: add allocator generic to mv store
  • core: make schema cloning fallible
  • schema: make sqlite_schema bootstrap allocation fallible
  • core/vdbe: make vec allocations fallible
  • core/translate: propagate fallible Vec allocations
  • migrate hash_table.rs / sorter.rs / order_by.rs to use fallible vec allocations
  • refactor Extendable trait to use Result for fallible allocations
  • core/alloc: clone Vec elements through TryClone
  • mvcc: use fallible skiplist allocations
  • Track MVCC checkpoint allocation sites

为什么这件事这么重要?

Rust 标准库的 Vec::push 在分配失败时是 abort——直接杀进程。对一个普通 CLI 工具,这没问题。对一个嵌入在你应用进程里的数据库,这是完全不可接受的:

用户 App 内存吃紧 → 数据库内部一次 Vec 扩容失败 → 整个 App 被 abort → 用户数据可能处于半写状态

SQLite 用 C 写,malloc 返回 NULL 就是普通的错误码,一路 propagate 上去变成 SQLITE_NOMEM这是 C 在这个场景下的天然优势。 Rust 想追上,必须手动把整条分配链路改成 fallible。

Turso 干的就是这件事,而且做得很彻底:

  1. 自定义 TursoAllocator
  2. Vec / Box<[T]> / skiplist / hash_table / sorter / order_by 的所有分配点改成返回 Result
  3. TryClone 替代 Clone
  4. Extendable trait 返回 Result
  5. 加了 OOM 故障注入器,主动在测试里制造分配失败
  6. 给每个分配点加标注(allocation site annotations),能追踪内存花在哪

这套工作量巨大、毫无宣传价值、外部用户完全感知不到。但它是「玩具」和「能嵌进别人 App 的数据库」之间的分界线。

我的观点:「Rust 重写 = 内存安全」是最肤浅的理解。 真正的价值在于,Rust 的类型系统让「分配可能失败」这件事变成可以在编译期强制、在测试期注入、在运行期追踪的一等公民。C 里你也能写 fallible 分配,但没人能保证你写全了;Rust 里 Result 不处理编译器就报警。

5.3 顺带一提:递归改迭代

还有两条容易被忽略但很关键的提交:

  • convert expr_walk* to use iteration instead of recursion
  • sqlite/parser: bound expression depth to prevent translator stack overflow

用户可以提交任意嵌套深度的 SQL 表达式。如果 AST 遍历是递归的,一条恶意构造的 SQL 就能爆栈。把递归改成迭代 + 给表达式深度设上限,这是嵌入式数据库必须做的攻击面收敛。


六、可靠性工程:他们怎么补上 SQLite 那套专有测试套件

前面说过,重写的最大障碍是「SQLite 的测试套件不开源」。Turso 的应对方式,是我在开源数据库里见过最认真的一套。

从仓库目录和 CI 配置能看到这些:

手段位置 / 证据作用
确定性模拟测试(DST)Dockerfile.antithesisantithesis: squash targeting diff into targeted_coverage.json用 Antithesis 平台在模拟的硬件/软件故障环境下跑,失败可完全复现
TLA+ 形式化验证tlaplus/sqlite-tx/对事务协议做形式化建模,在写代码之前证明协议正确
Simulatortesting/simulator: support virtual columns随机生成 SQL 负载 + 随机注入故障
Stress / Whopperturso_stressAdd recovery-heavy profile to Whopper压力测试,专门有「恢复密集型」profile
Shuttle 并发测试testing/stress: reset shuttle step counter on iteration progress系统性探索线程交错
Aristo WAL 验证.aristo/Add Aristo WAL verification专门验证 WAL 的正确性
模糊测试fuzz/lock cargo-fuzz to 0.13.0parser 与执行路径的 fuzzing
OOM 故障注入out-of-memory fault injector主动制造分配失败
一致性对拍tools/dbhashtesting/conformance/javascript与 SQLite 做结果对拍
性能回归 CICodSpeed,ci: shard CodSpeed benchmarks by toolchain每个 PR 都跑基准,防性能回退

这套组合拳的思路很清晰:SQLite 用「三十年 + 海量人工测试用例」堆信心,Turso 用「形式化验证 + 可复现的随机」堆信心。

后者理论上更强——TLA+ 能覆盖人想不到的交错,DST 能让「三个月才出现一次的诡异 bug」变成可以按需重放的确定性场景。前者胜在时间检验。

顺便提一个有意思的观察:CHANGELOG 的提交者列表里有一个叫 roboturso 的账号,提交内容包括:

  • core/translate: use abort journal analysis for upsert DO UPDATE arms
  • core/mvcc: reset index shadow finger when index keys are created mid-scan
  • core: classify virtual tables by parsing schema SQL
  • sqlite/parser: bound expression depth to prevent translator stack overflow

而且仓库里有 .claude/skills.codexAGENTS.mdCLAUDE.md,CONTRIBUTING.md 里专门有 Add section on AI codingAdd some testing guidance for agents,甚至有 fossier: vouch for @roboturso 这样的信任背书提交。

这是我第一次在一个严肃的数据库项目里,看到 AI agent 作为常规 contributor 出现在 changelog 里,而且提交的是 MVCC 索引游标这种硬核补丁。 你可以说这很激进,但换个角度:一个有 DST + TLA+ + fuzzing + OOM 注入 + 性能回归 CI 的项目,恰恰是最有资格让 AI 提交代码的项目——因为它的验证网足够密。测试基础设施的强度,决定了你能接受多快的代码产出速度。


七、上手实战

7.1 装、跑、连

# 安装 CLI
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/tursodatabase/turso/releases/latest/download/turso_cli-installer.sh | sh

# 或者 brew
brew install turso

# 交互式 shell (默认内存库)
tursodb

# 打开持久化文件
tursodb ./app.db
$ tursodb
Turso
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database
turso> CREATE TABLE users (id INT, username TEXT);
turso> INSERT INTO users VALUES (1, 'alice'), (2, 'bob');
turso> SELECT * FROM users;
┌────┬──────────┐
│ id │ username │
├────┼──────────┤
│ 1  │ alice    │
├────┼──────────┤
│ 2  │ bob      │
└────┴──────────┘

几个值得记的 CLI 开关:

tursodb --mcp                            # 起 MCP server 而不是交互 shell
tursodb --experimental-encryption ./a.db # 静态加密(实验)
tursodb --experimental-views ./a.db      # 视图(实验)
tursodb --experimental-multiprocess-wal ./a.db  # 多进程 WAL(实验)
tursodb -m list ./a.db                   # 输出格式对齐 sqlite3, 方便脚本对拍
tursodb --readonly ./a.db                # 只读打开

-m list 这个开关在做 SQLite 对拍时特别有用,输出格式和 sqlite3 默认一致,可以直接 diff。

7.2 各语言接入

# Rust
cargo add turso

# JavaScript / TypeScript (含 WASM)
npm i @tursodatabase/database

# Python
uv pip install pyturso

# Go
go get turso.tech/database/tursogo
// JavaScript —— 注意 connect 是 async, 但 prepare/all 是同步的
import { connect } from '@tursodatabase/database';

const db = await connect('sqlite.db');

// batch() 返回每条语句各自的结果
const results = await db.batch([
  "CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)",
  "INSERT OR REPLACE INTO kv VALUES ('a', '1')",
  "SELECT * FROM kv",
]);

const users = db.prepare('SELECT * FROM users WHERE id > ?').all(10);
console.log(users);
# Python —— DB-API 2.0 风格, 从 sqlite3 迁移几乎零成本
import turso

con = turso.connect("sqlite.db")
cur = con.cursor()
cur.execute("SELECT * FROM users")
print(cur.fetchone())

# 两个 sqlite3 没有的实用能力(CHANGELOG: release the GIL and expose
# interrupt() / set_query_timeout())
con.set_query_timeout(5000)   # 毫秒级查询超时
# 另一线程可以调 con.interrupt() 打断慢查询

Python 绑定「release the GIL」这一条对 CPU 密集的分析查询很关键——查询跑在 Rust 侧时不持有 GIL,其他 Python 线程能继续跑。sqlite3 标准库在这方面就没这么干净。

// Go —— 标准 database/sql 接口
import (
    "database/sql"
    _ "turso.tech/database/tursogo"
)

conn, _ := sql.Open("turso", "sqlite.db")
defer conn.Close()

stmt, _ := conn.Prepare("select id, username from users")
defer stmt.Close()

rows, _ := stmt.Query()
for rows.Next() {
    var id int
    var username string
    rows.Scan(&id, &username)
    fmt.Printf("User: %d %s\n", id, username)
}

7.3 内置 MCP server:数据库自己长出了 AI 接口

这是 Turso 一个很有前瞻性的设计——CLI 内置 Model Context Protocol server

tursodb ./analytics.db --mcp

配到 MCP 客户端里:

{
  "mcpServers": {
    "turso": {
      "command": "/path/to/.turso/tursodb",
      "args": ["/path/to/your/database.db", "--mcp"]
    }
  }
}

Claude Code 一行搞定:

claude mcp add my-project-db -- tursodb ./data/app.db --mcp

暴露九个工具:open_databasecurrent_databaselist_tablesdescribe_tableexecute_query(只读 SELECT)、insert_dataupdate_datadelete_dataschema_change

也可以直接走 JSON-RPC 裸调,方便写集成测试:

cat << 'EOF' | tursodb --mcp
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"client","version":"1.0"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"schema_change","arguments":{"query":"CREATE TABLE users (id INTEGER, name TEXT)"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_tables","arguments":{}}}
EOF

注意这里的权限设计:execute_query 明确限定只读 SELECT,写操作拆成独立的 insert_data / update_data / delete_data / schema_change 这个拆分不是为了好看,是为了让 MCP 客户端能做工具级授权——你可以只放开读工具,禁掉所有写工具。相比「给 Agent 一个万能 execute_sql」,这是负责任得多的设计。

一句话总结这个功能的意义:数据库自带 MCP,意味着「AI 能查我的数据」这件事不再需要中间层。 对本地 Agent 场景(个人知识库、日志分析、代码库索引),这省掉的是一整个服务。

7.4 迁移策略:影子双写 + dbhash 对拍

如果你真想从 SQLite 迁到 Turso,别一刀切。仓库里有个 tools/dbhash,配合 -m list 输出模式,可以搭一套影子校验:

#!/usr/bin/env bash
# shadow_verify.sh —— SQLite / Turso 双跑对拍
# 用法: ./shadow_verify.sh ./queries.sql ./data.db
set -euo pipefail

QUERIES="$1"
DB="$2"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

# 1) 各复制一份, 避免互相干扰
cp "$DB" "$WORK/sqlite.db"
cp "$DB" "$WORK/turso.db"

# 2) 同一批语句分别在两个引擎上执行
sqlite3 "$WORK/sqlite.db" < "$QUERIES" > "$WORK/out.sqlite" 2> "$WORK/err.sqlite" || true
tursodb -q -m list "$WORK/turso.db" < "$QUERIES" > "$WORK/out.turso" 2> "$WORK/err.turso" || true

# 3) 对比查询输出
if diff -u "$WORK/out.sqlite" "$WORK/out.turso" > "$WORK/diff.out"; then
  echo "[OK]   查询结果一致"
else
  echo "[DIFF] 查询结果不一致:"
  head -50 "$WORK/diff.out"
fi

# 4) 对比落盘后的逻辑内容
H1=$(dbhash "$WORK/sqlite.db")
H2=$(dbhash "$WORK/turso.db")
if [ "$H1" = "$H2" ]; then
  echo "[OK]   数据库内容哈希一致: $H1"
else
  echo "[DIFF] 内容哈希不一致: sqlite=$H1 turso=$H2"
fi

有一个坑必须提前知道:Turso 不保证查询结果的顺序与 SQLite 一致(manual 的 Limitations 里明确写了,对应 issue #2964)。所以对拍脚本里,所有 SELECT 都必须带完整确定的 ORDER BY,否则你会被大量假阳性 diff 淹没。

顺便说,这个限制本身也是个警告:如果你的业务代码依赖了「没有 ORDER BY 但结果碰巧有序」这件事,那你在任何数据库上都是在赌博。 Turso 只是把这个赌局提前引爆了。


八、性能:什么负载受益,什么不受益

Turso 的定位是 in-process 数据库,读写延迟目标是亚微秒级(没有网络往返)。但「重写成 Rust」本身不等于更快。从 CHANGELOG 里能看到几条真实的优化:

已经落地的:

  • perf(vdbe): reuse btree cursor on OpenRead instead of rebuilding —— OpenRead 时复用 B-tree cursor 而不是重建。这对循环内反复打开同一张表的执行计划收益明显。
  • perf(vdbe): only build the comparator and clone the aggregate arg when needed —— 聚合参数惰性 clone。
  • Eliminate quadratic performance for prepare of queries with a lot of parameters —— 参数多的语句 prepare 从 O(n²) 降到线性。批量 INSERT 用几百个占位符的场景直接受益。
  • avoid full range scan for index seek operation —— 索引 seek 不再退化成全范围扫描。
  • Linear better than quadratic —— 同类优化。
  • Page cache soft limit + Eliminate blocking I/O in page cache spill path —— 页缓存软上限,溢出路径不阻塞。
  • core/mvcc: serialize buffer directly, avoid 2x buffer heap size —— MVCC 序列化不再开双份缓冲。
  • mem usage in MVCC: pack timestamp begin/end in RowVersion —— 时间戳打包进 RowVersion,缩小版本链内存。
  • core/mvcc: Shrink version chains and remove empty slots at checkpoint —— checkpoint 时收缩版本链。
  • introduce ImmutableRecordRef to reduce cloning —— 减少记录 clone。
  • core/optimizer: improve starting table selection in greedy join —— 贪心 join 的起始表选择改进。

判断受益 / 不受益的经验规则:

场景判断原因
async runtime 里的高并发小查询✅ 明显受益不再 spawn_blocking,io_uring 全程非阻塞
参数极多的批量 INSERT✅ 明显受益prepare 从 O(n²) 变线性
多写者场景(且能接受无索引)✅ 受益BEGIN CONCURRENT 突破单写者
需要同时被 psql 工具链访问的嵌入式库✅ 独一份没有第二个 in-process 引擎有 pgwire
单线程顺序读、小数据集⚠️ 基本持平SQLite 本来就在这个场景做到极致了
大库 + 复杂 join + 重索引❌ 暂不建议优化器成熟度差 SQLite 二十年
需要 vacuum / savepoint / 稳定 trigger❌ 不支持manual 明确列为限制
MVCC + 大数据集❌ 危险全量入内存,启动慢且吃爆内存

测性能的正确姿势:别信任何人给的 benchmark(包括这篇文章),用你自己的真实 query 集跑对拍。Turso 自己在 CI 上接了 CodSpeed 做每 PR 的性能回归追踪,还按 toolchain 和 benchmark 分片 —— 这说明他们自己也知道性能是要持续盯的,不是发个数字就完事。


九、风险清单:我认为你现在必须知道的几件事

9.1 文档和实现存在明显时间差

这一点我要特别标出来,因为它会直接坑到人。

docs/manual.md 的 Limitations 章节说:

  • No multi-process access
  • No triggers
  • No views
  • No savepoints
  • No vacuum

但 README 的 Features 里写着:

  • Multi-process WAL coordination via the .tshm sidecar(实验)

CLI 也有 --experimental-views 开关。而 CHANGELOG 里有 core/storage: add savepoint rollback consistency intentcore/pager: reset pager state on savepoint rollbackcore/mvcc: rollback savepoint on abandoned statement —— savepoint 显然在做了。还有 core/btree: invalidate shape caches on peer writes; drop trigger workaround —— trigger 也有动静。

结论:manual 的限制清单落后于实现。 遇到冲突时,以 README.md + COMPAT.md + CHANGELOG 为准,manual 当参考。更稳妥的做法是:你关心的每一个特性,都自己写个五行的最小复现验证一遍,别信文档。

9.2 实验性特性的密度非常高

统计一下带 experimental 标记的东西:Postgres 兼容、静态加密、DBSP 增量计算、tantivy 全文检索、多进程 WAL、索引(manual 里写 Indexes are currently experimental in Turso and not enabled by default)、views、page codecs、index method。

索引默认不启用这一条最要命。一个默认没索引的数据库,能跑的负载类型是极其有限的。

9.3 版本号还是 0.x,但 README 说「已在多家组织的生产环境运行」

原文:It runs in production today at multiple organizations. 同时 CHANGELOG 里有一条 Drop the beta warning

这两件事同时为真是可能的——在受控场景下(数据量可控、SQL 模式固定、有完整回滚方案、不开 MVCC 不开 PG 前端)。但它绝不等于「你可以拿它替换生产 SQLite」。

我的建议按风险分三档:

档位场景建议
🟢 可以上本地工具、CLI、开发环境、可重建的缓存库、AI Agent 的临时记忆库直接用,出问题重建即可
🟡 可以试需要 async I/O 的服务、需要 pgwire 的嵌入式场景影子双写 + dbhash 对拍,保留 SQLite 回滚路径
🔴 不要碰用户核心数据、金融记录、无备份的单一数据源、开 MVCC 的大库等 1.0,等索引稳定

9.4 Postgres 前端的定位要摆正

不要把它理解成「Turso 可以替代 PostgreSQL」。它是「你的嵌入式数据库可以被 Postgres 生态的工具访问」。

这个区别很重要。真正的用例是:

  • 本地开发时用 psql / DBeaver / pgAdmin 直接查嵌入式库,不用装第二套工具
  • 已有的 Postgres 驱动和部分 ORM 可以复用,减少胶水代码
  • 边缘节点跑嵌入式引擎,中心用 Postgres,SQL 方言可以尽量统一
  • 测试环境用嵌入式 Turso 顶替 Postgres 容器,起停快得多

而不是:把生产 Postgres 集群换成 Turso。缺的东西太多了:没有 EXPLAIN(一个都没有)、没有 information_schema 视图、没有 LISTEN/NOTIFY、没有 advisory lock、没有大对象、没有 range/multirange 类型、没有 BRIN/GiST 索引、pg_roles 只有一个硬编码的 turso 角色。


十、总结:这个项目真正的价值在哪

回到开头那个问题:「数据库界的 LLVM」是不是营销话术?

我的结论是:架构上成立,成熟度上还早,但方向是对的。

三个理由:

第一,VDBE 作为 IR 的复用是真实的技术资产,不是包装。 SQLite 二十多年前就把 SQL 编译成字节码这件事做对了,只是没人想过前端可以换。Turso 把这个隐藏资产显式化了。一旦这条路走通,加第三个、第四个前端(MySQL 方言?某种时序 DSL?)的边际成本会大幅下降——这正是 LLVM 模型的核心价值:新增前端的成本远低于新建一个数据库

第二,Rust 重写的真正红利被大多数人理解错了。 不是内存安全(SQLite 用 C 写了三十年也没爆过多少内存漏洞),而是:

  • 全链路异步 I/O —— fork 一份 C 代码是做不到的,这是架构级改动
  • 可失败的内存分配 —— 让「OOM 不 abort 宿主进程」成为编译期强制、测试期可注入、运行期可追踪的性质
  • 可运行时注册的 I/O 后端 —— 把引擎接到任意 runtime 上

这三条,每一条都是「重写」才能拿到,「fork」拿不到的。这也回答了「为什么要重写」这个最初的问题。

第三,可靠性工程的投入配得上它的野心。 TLA+ 形式化验证 + Antithesis DST + simulator + shuttle 并发测试 + fuzzing + OOM 故障注入 + CodSpeed 性能回归 —— 这套组合在开源数据库里属于第一梯队。它在明确地对标 SQLite 那套不开源的 TH3。

但坦率说,现在还不是用它替换 SQLite 的时候。 索引默认不开、MVCC 全量入内存、文档落后于实现、一堆实验性特性、Postgres 前端的静默语义丢弃——每一条都够你在生产环境上一次强度不小的课。

那什么时候该关注它?现在。

  • 如果你在 Rust / Node.js / Python 里做 async 服务,且被 SQLite 的同步 API 恶心过 —— 现在就可以试
  • 如果你在做本地 AI Agent,需要一个自带 MCP、能跑在 WASM 里、能加密的嵌入式库 —— 现在就可以试
  • 如果你被 SQLite 的单写者卡住过 —— 关注 BEGIN CONCURRENT,等索引支持落地
  • 如果你在做边缘/多租户,希望嵌入式库也能被 psql 工具链访问 —— 关注 Postgres 前端,但先跑一遍兼容性探针

最后留一个更大的问题。

SQLite 是世界上部署量最大的软件之一,靠的是「极致可靠 + 极致简单 + 三十年不变」。Turso 走的是相反的路:极致激进 + 特性爆炸 + 用形式化验证和故障注入去兜可靠性的底

这两条路,长期看谁赢?

我倾向于认为它们不冲突。SQLite 会继续统治那些「装进去就再也不用管」的场景——手机、浏览器、飞机、路由器。而 Turso 争夺的是一块新战场:需要嵌入式的低延迟,但又要求 MVCC、异步、加密、向量、增量视图、多方言的现代应用。这块地在 2020 年之前几乎不存在,是 edge computing 和本地 AI Agent 硬生生撑出来的。

从这个角度看,Turso 最聪明的地方,可能不是用 Rust 重写了 SQLite,而是它意识到「嵌入式数据库」这个品类的需求变了,然后按新需求重新设计了一遍,只是顺便保持了 SQLite 兼容

顺便保持兼容——这四个字,才是它最大的护城河。


参考与验证入口(本文所有技术细节均可在以下位置核对):

  • 主仓库:github.com/tursodatabase/turso(MIT 协议)
  • SQLite 兼容矩阵:仓库根目录 COMPAT.md
  • Postgres 兼容矩阵:postgres/COMPAT.md
  • 事务与 MVCC 语义:docs/manual.md 的 Transactions 章节
  • 性能说明:PERF.md
  • 形式化规格:tlaplus/sqlite-tx/
  • 最新版本:v0.8.0-pre.3(2026-08-06 发布)

推荐文章

最全面的 `history` 命令指南
2024-11-18 21:32:45 +0800 CST
goctl 技术系列 - Go 模板入门
2024-11-19 04:12:13 +0800 CST
使用 Go Embed
2024-11-19 02:54:20 +0800 CST
程序员茄子在线接单