sqlite-utils 4.0:数据库迁移与改进
2026年7月7日
今天上午,我发布了sqlite-utils 4.0——这是该项目的第124个版本,也是自2020年11月3.0以来首次大版本更新。除了若干微小但关键的破坏性变更(详见升级指南),本次版本还带来三项核心新功能:数据库迁移、嵌套事务(通过新增的db.atomic()方法实现),以及对复合外键的支持。
基于sqlite-utils的数据库 schema 迁移
Schema 迁移用于定义对 SQLite 数据库的一系列变更,同时提供机制跟踪已执行的迁移,并自动应用尚未完成的任务。
迁移需通过sqlite-utils Python 库在 Python 文件中定义,该库包含功能强大的table.transform()方法,可实现 SQLite 原生ALTER TABLE语句不支持的增强型表修改能力。
(table.transform()采用了SQLite 官方文档推荐的模式:先创建符合新 schema 的临时表,复制原表数据,再删除原表并将临时表重命名为原表名称。)
以下是一个迁移文件示例:先创建名为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
随后查看数据库 schema:
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表则是完成全部三次迁移后的最终 schema。
如需查看已执行和待执行的迁移列表,运行以下命令:
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()实例定义的迁移。
你也可以通过Python 代码调用migrations.apply(db)方法执行迁移,这对需要跨版本管理自身数据库 schema 的工具开发十分实用。我自己的LLM 工具已采用类似模式多年,具体可参考llm/embeddings_migrations.py。
参考方案
我最欣赏的同类实现仍是Django Migrations,由 Andrew Godwin 在其早期项目South的基础上开发。说个趣事:2008 年首届 DjangoCon 大会的Schema Evolution 专题讨论上,Andrew、Russ Keith-Magee 和我各自展示了针对 Django 的 schema 迁移方案!我当时的方案名为dmigrations,是与伦敦 Global Radio 的团队共同开发的。
Django 迁移可通过模型定义自动生成,还支持回滚到之前的版本。而sqlite-utils的方案刻意做了简化:与 Django 不同,sqlite-utils更倾向于通过代码直接创建表,而非基于模型定义的 ORM,因此无法自动生成迁移。
我决定不支持回滚功能,因为根据我的经验,这一特性极少被用到。对于 SQLite 项目,实现回滚的简单方法是:在执行迁移前复制一份数据库文件即可!
从 sqlite-migrate 迁移而来
sqlite-utils的迁移功能设计至今已有三年——最初它作为独立包sqlite-migrate发布,但始终停留在测试版阶段。
经过多场景验证,我对这一设计已足够自信,因此决定将其整合进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 版本包含若干微小的向后不兼容修复(因此升级为大版本),同时引入三项核心新功能:
这是我眼中本次版本的标志性新功能,也是撰写这篇博文的原因。
长期以来,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)
这一功能的由来是:我让代码助手梳理所有待处理的 issue 和 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 数据自动检测列类型(文本、整数、实数)。现在我将其设为默认行为,而大版本更新正好提供了这样做的契机。
table.extract()和extracts=参数不再为全null值创建查找表记录。(#186)
这是本次版本解决的最古老的 issue——底层 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 撰写,发布说明亦是如此。
这类文档我逐渐放心交给 AI 来完成——它不需要说服任何人,也不需要表达观点,只需要尽可能准确、详尽。我已仔细审阅了发布说明,确认其内容准确全面。
Claude Fable 5 功不可没
sqlite-utils 4.0 的首个 alpha 版本是一年多前发布的。我迟迟没有推出稳定版,是因为大版本更新意味着要梳理并修复大量小的设计缺陷,工作量巨大。
Claude Fable 5(以及一定程度上的 Opus 4.8 和 GPT-5.5)的帮助,让我终于克服了惰性,得以充分利用能投入到这个库上的时间。
Fable 在 API 设计上极具品味,只要给出开放的目标,它就会积极主动地推进。我最成功的一次提示,是针对我认为最终的候选版本发起的评审任务:
review the changes on main since the last tagged 3.x release - I am about to ship them as sqlite-utils 4.0, a stable version that promises no backwards-incompatible fixes for a very long time.
review the changelog and upgrade guide, and write yourself scratch scripts to try out all of the new features in v4 - save those scripts but don't commit them
我分别在 Codex Desktop 中使用 GPT-5.5 xhigh,以及在 Claude Code 中使用 Fable 5 进行了测试。
GPT-5.5 编写了5 个 Python 脚本,但没有发现什么特别的问题——其最终报告在此。
而 Fable 5 编写了12 个脚本,在报告中指出了 4 个发布阻断问题和 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逐一解决了这些问题。
毫无疑问,如果没有最新大模型的协助,sqlite-utils 4.0 的质量绝不可能达到如今的高度。