返回 2026-07-09
🛠 工具 / 开源

sqlite-utils 4.0 发布,现已支持数据库模式迁移sqlite-utils 4.0, now with database schema migrations

simonwillison.net·2026-07-07

sqlite-utils 4.0 正式发布,这是该项目自 2020 年 11 月的 3.0 版本以来的首个大版本更新(总计第 124 个版本)。新版本引入了三大主要功能,其中最核心的是新增了数据库模式迁移支持。此次更新还包含了一些虽小但意义重大的破坏性变更,用户在升级时需参考官方提供的升级指南以避免兼容性问题。

Simon Willison

2026年7月7日

今天早上我发布了 sqlite-utils 4.0,这是该项目的第 124 次发布,也是自 2020 年 11 月的 3.0 版本以来的首次大版本更新。除了此升级指南中描述的一些虽小但重要的破坏性变更外,该版本还引入了三大主要功能:数据库迁移、嵌套事务(通过新的 db.atomic() 方法)以及对复合外键的支持。

使用 sqlite-utils 进行数据库模式迁移

模式迁移定义了对 SQLite 数据库要进行的一系列更改,并提供了一种机制来跟踪哪些迁移已应用,以及应用任何处于待处理状态的迁移。

迁移是在 Python 文件中使用 sqlite-utils Python 库定义的,该库包含一个强大的 table.transform() 方法,提供了 SQLite 的 ALTER TABLE 语句所不支持的增强型表修改功能。

(table.transform() 实现了 SQLite 文档推荐的模式——创建一个带有新模式的新临时表,将数据复制过去,然后删除旧表,并将临时表重命名以替换旧表。)

这是一个迁移文件示例,它创建了一个名为 creatures 的表,在第二步中向其中添加了一个额外的列,然后在第三步中更改了其中两列的类型:

from sqlite_utils import Migrations

migrations = Migrations("creatures")

@migrations()
def create_table(db):
    db["creatures"].create(
        {"id": int, "name": str, "species": str},
        pk="id",
    )

@migrations()
def add_weight(db):
    db["creatures"].add_column("weight", float)

@migrations()
def change_column_types(db):
    db["creatures"].transform(types={"species": int, "weight": str})

将其保存为 migrations.py,并像这样在一个全新的数据库上运行它:

uvx sqlite-utils migrate data.db migrations.py

然后如果你检查该数据库的模式:

uvx sqlite-utils schema data.db

你会看到以下 SQL:

CREATE TABLE "_sqlite_migrations" (
   "id" INTEGER PRIMARY KEY,
   "migration_set" TEXT,
   "name" TEXT,
   "applied_at" TEXT
);
CREATE UNIQUE INDEX "idx__sqlite_migrations_migration_set_name"
    ON "_sqlite_migrations" ("migration_set", "name");
CREATE TABLE "creatures" (
   "id" INTEGER PRIMARY KEY,
   "name" TEXT,
   "species" INTEGER,
   "weight" TEXT
);

_sqlite_migrations 表用于跟踪哪些迁移函数已经运行。上面的 creatures 表是应用了所有三个迁移后的模式。

要查看待处理和已应用的迁移列表,请运行以下命令:

uvx sqlite-utils migrate data.db migrations.py --list

输出:

Migrations for: creatures

  Applied:
    create_table - 2026-07-07 17:58:41.360051+00:00
    add_weight - 2026-07-07 17:58:41.360608+00:00
    change_column_types - 2026-07-07 18:01:15.802000+00:00

  Pending:
    (none)

如果不指定迁移文件,sqlite-utils migrate data.db 命令将扫描当前目录及其子目录中名为 migrations.py 的文件,并应用在其中找到的任何 Migrations() 实例。

你还可以使用 migrations.apply(db) 方法从 Python 代码中执行迁移,这对于构建在多个版本中管理自身数据库模式的工具非常有用。我自己的 LLM 工具已经使用这种模式好几年了,如 llm/embeddings_migrations.py 所示。

已有实现

我最喜欢的这种模式的实现仍然是 Django 的 Migrations,由 Andrew Godwin 基于他早期的 South 项目开发。有趣的是:早在 2008 年的第一届 DjangoCon 上,Andrew、Russ Keith-Magee 和我就在 Schema Evolution 小组讨论会上展示了我们各自针对 Django 模式迁移的竞争方案!我的尝试叫做 dmigrations,是与伦敦 Global Radio 的一个团队共同开发的。

Django 的迁移可以从模型定义自动生成,并包含回滚到先前版本的能力。sqlite-utils 的方案则有意做得更简单:与 Django 不同,sqlite-utils 鼓励以编程方式创建表,而不是使用基于模型定义的 ORM,因此我们没有任何可用于自动生成迁移的基础。

我决定跳过回滚功能,因为根据我的经验,这是一个很少被使用的功能。对于 SQLite 项目,实现回滚的一个简单方法是:在应用迁移之前创建数据库文件的副本!

从 sqlite-migrate 迁移

sqlite-utils 迁移的设计现在已经有三年历史了——我最初将其作为一个名为 sqlite-migrate 的独立包发布,但它一直没有超出测试版(beta release)的阶段。

现在我已经在足够多的地方使用了该包,对其设计充满信心,因此我决定将其提升为 sqlite-utils 的一项功能,使其默认可供日益壮大的 sqlite-utils/Datasette/LLM 生态系统中的所有其他工具使用。

我发布了 sqlite-migrate 的最后一个版本,将其依赖项切换为 sqlite-utils>=4,并用以下内容替换了 __init__.py 文件:

from sqlite_utils import Migrations

__all__ = ["Migrations"]

任何依赖 sqlite-migrate 的现有项目都应该无需修改即可继续正常工作。

sqlite-utils 4.0 中的其他所有内容

以下是此版本的发行说明,附带了一些内联注释:

4.0 版本包含一些轻微的向后不兼容修复(因此提升了主版本号),并引入了三项主要新功能:数据库迁移,提供了一种结构化机制来随时间演进项目的架构(schema)。(#752)

我将数据库迁移视为标志性的新功能,因此写了这篇博文。

通过 db.atomic() 支持嵌套事务,并对整个库中的事务工作方式进行了大量改进。(#755)

sqlite-utils 与数据库事务之间的关系长期以来一直比较混乱,部分原因是当我在 2018 年开始设计这个库时,我对事务在 SQLite 本身中是如何运作的还没有很深的体会。

将迁移功能添加到核心库让我下定决心最终解决这个难题,因为事务能让迁移系统安全得多,也更容易推断。

我最终围绕一个 db.atomic() 上下文管理器构建了此功能,它看起来像这样:

with db.atomic():
    db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
    db.table("dogs").insert({"id": 2, "name": "Pancakes"})

SQLite 支持 Savepoints(保存点),因此 db.atomic() 可以嵌套,从而在事务内部执行事务。这非常巧妙!

支持复合外键,包括通过 table.foreign_keys 进行创建、转换和内省。(#594)

这源于我让一个编程代理审查了所有待处理的问题和 PR,以找出应该包含在 4.0 版本中的内容,因为如果以后再添加它们就会造成破坏性变更,而它准确地识别出复合外键正是这类功能。

我首先对 table.foreign_keys 内省方法进行了破坏性更改,然后决定看看 Claude Fable 5 能否处理将复合外键创建集成到库中这项更繁琐的工作。它协助创建的 API 设计让我感觉恰到好处——与库其余部分的工作方式保持一致。

其他值得注意的更改包括:Upserts 现在使用 SQLite 的 INSERT ... ON CONFLICT ... DO UPDATE SET 语法,自动检测现有表的主键,并拒绝缺少所需主键值的记录。(#652)

正是这一变更促使我最初考虑进行包含破坏性变更的 4.0 版本升级。我构建此功能是为了帮助支持 sqlite-chronicle,该工具使用触发器来跟踪表中已被插入、更新或删除的行。

db.query() 现在会立即执行,并拒绝不返回数据行的语句;写入和 DDL 操作请使用 db.execute()。

这可能是破坏性最强的变更——因此,我不得不更新自己代码中的几处地方,将 db.query() 切换为 db.execute()。

CSV 和 TSV 导入现在默认会检测列类型,而插入现有表时会保留这些表的列类型。(#679)

sqlite-utils insert data.db creatures creatures.csv --detect-types 标志是后来添加的,用于允许根据 CSV 中的数据自动检测列类型(text、integer、real)。它应该成为默认选项,而发布 4.0 版本意味着我可以做到这一点。

table.extract() 和 extracts= 不再为全空值(all-null values)创建查找表记录。(#186)

此版本解决的最古老的问题——其底层 bug 是在 2020 年 10 月(由我)提交的。

有关向后不兼容更改的详细信息,请参阅《从 3.x 升级到 4.0》。在 4.0 预发布周期内交付的功能和修复的详细发行说明可在 4.0a0、4.0a1、4.0rc1、4.0rc2、4.0rc3 和 4.0rc4 中查看。

升级指南完全由 Claude Fable 5、Claude Opus 4.8 和 GPT-5.5 编写。发行说明也是如此。

这类文档正是我慢慢开始习惯外包给机器人的内容。它不需要说服任何人,也不需要表达任何观点——它的任务是尽可能准确和详细。我已经仔细审阅了发行说明,可以确认它们既准确又全面。

Claude Fable 5 帮了大忙

一年多前,我发布了 sqlite-utils 4.0 的第一个 alpha 版本。我之所以在发布稳定版上一直拖延,是因为要追踪和清理许多其他次要设计缺陷需要耗费大量精力,而大版本号的更新让我得以着手处理这些缺陷。

来自 Claude Fable 5(以及在较小程度上 Opus 4.8 和 GPT-5.5)的协助,正好为我提供了所需的动力,帮助我克服了惯性,并充分利用了我能腾出投入到这个库上的时间。

Fable 在 API 设计方面品味极佳,如果你给它一个更开放的目标,它会表现得极其主动。我最成功的一次提示词是一项审查任务,该任务针对的是我原以为的最后一个候选发布版本(release candidate):

审查自上一个标记的 3.x 版本发布以来 main 分支上的更改 - 我准备将它们作为 sqlite-utils 4.0 发布,这是一个稳定版本,承诺在很长一段时间内不会有向后不兼容的修复。审查更新日志和升级指南,并自己编写一些临时脚本来尝试 v4 中的所有新功能 - 保存这些脚本,但不要提交它们。

我在 Codex Desktop 中使用 GPT-5.5 xhigh,并在 Claude Code 中使用 Fable 5 尝试了此操作。

GPT-5.5 编写了 5 个 Python 脚本,但没有发现任何特别有趣的内容——它的最终报告在这里。

Fable 5 编写了 12 个脚本,在其报告中找出了 4 个发布阻碍问题(release blockers)和 10 个额外问题,并构建了一个整洁的组合复现脚本,该脚本在运行时输出了以下内容:

=== 1. Failed db.execute() write leaves an implicit transaction open ===
  in_transaction after failed write: True
  BUG: table 'other' silently lost when connection closed

=== 2. Leading ';' bypasses the query() first-token scanner ===
  BUG: raised OperationalError: no such savepoint: sqlite_utils_query
  BUG: row persisted despite rollback (count=1)

=== 3. Rejected write PRAGMA via query() still takes effect ===
  BUG: user_version=5 after 'rejected' statement (docs say no effect)

=== 4. Implicit compound FK resolves pk columns in table order, not PK order ===
  BUG: other_columns reported as ('b', 'a'), should be ('a', 'b')
  BUG: transform of valid data raised IntegrityError: FOREIGN KEY constraint failed

=== 5. ForeignKey (now a dataclass) is no longer hashable ===
  BUG: cannot use 'sqlite_utils.db.ForeignKey' as a set element (unhashable type: 'ForeignKey')

=== 6. Mixed ForeignKey objects and tuples in foreign_keys= rejected ===
  BUG: foreign_keys= should be a list of tuples

=== 7. insert --csv into an EXISTING table transforms its column types ===
  BUG: existing zip '01234' is now 1234 (column type: int)

=== 8. insert(pk=, alter=True) regression: InvalidColumns before alter runs ===
  BUG: InvalidColumns: Invalid primary key column ['id'] for table t with columns ['a']

=== 9. migrate --stop-before an already-applied migration applies everything ===
  BUG: m2 was applied despite --stop-before m1 (m1 already applied)

=== 10. ensure_autocommit_on() silently commits an open transaction ===
  BUG: row survived rollback (count=1) - transaction was committed

我发现自己几乎同意所有的观点。这是包含 16 次提交的 PR,我们通过它依次解决了这些问题。

我毫不怀疑,如果没有最新前沿模型(frontier models)的协助,sqlite-utils 4.0 的发布质量绝对达不到现在这么高的水平。

需要完整排版与评论请前往来源站点阅读。