sqlite-utils 4.0,现已支持数据库模式迁移

Simon Willison's Blog 工具

摘要

sqlite-utils 4.0 引入了数据库模式迁移、嵌套事务和复合外键,使得以编程方式管理 SQLite 数据库模式变更更加容易。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/07 22:43

# sqlite-utils 4.0:现已支持数据库模式迁移 来源:https://simonwillison.net/2026/Jul/7/sqlite-utils-4/ 2026年7月7日 今天早上我发布了 sqlite-utils 4.0 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0),这是该项目的第124个版本,也是自2020年11月 3.0 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-0) 以来的首个主版本号提升。除了几个小但重要的不兼容变更(详见 升级指南 (https://sqlite-utils.datasette.io/en/stable/upgrading.html)),该版本引入了三大特性:**数据库迁移**、**嵌套事务**(通过新的 `db.atomic()` 方法),以及对**复合外键**的支持。 #### 使用 sqlite-utils 进行数据库模式迁移 模式迁移定义了一系列对 SQLite 数据库的更改操作,并附带一种机制,用于跟踪哪些迁移已应用以及应用任何待处理的迁移。 迁移使用 sqlite-utils Python 库 (https://sqlite-utils.datasette.io/en/stable/python-api.html) 在 Python 文件中定义,该库包含一个强大的 `table.transform()` 方法,能提供 SQLite 的 `ALTER TABLE` 语句所不支持的增强型表修改能力 (https://sqlite-utils.datasette.io/en/stable/python-api.html#transforming-a-table)。 (`table.transform()` 实现了 SQLite 文档推荐的模式 (https://www.sqlite.org/lang_altertable.html#otheralter):创建一个具有新模式的临时新表,将数据复制过去,然后删除旧表并将临时表重命名为原表名。) 以下是一个迁移文件示例,它首先创建一个名为 `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()` 实例。 你也可以通过 Python 代码 (https://sqlite-utils.datasette.io/en/stable/migrations.html#applying-migrations-in-python) 执行迁移,使用 `migrations.apply(db)` 方法,这对于构建跨多个版本管理自身数据库模式的工具非常有用。我自己的 LLM 工具 (https://llm.datasette.io/) 多年来一直使用该模式的某个版本,如 llm/embeddings_migrations.py (https://github.com/simonw/llm/blob/0.31/llm/embeddings_migrations.py) 所示。 #### 已有成果 我最喜欢的该模式实现仍然是 Django 的迁移 (https://docs.djangoproject.com/en/6.0/topics/migrations/),由 Andrew Godwin 基于他早前的项目 South (https://github.com/andrewgodwin/south) 开发。有趣的事实:在 2008 年首届 DjangoCon 上,Andrew、Russ Keith-Magee 与我在 模式演变讨论小组 (https://www.youtube.com/watch?v=VSq8m00p1FM) 共同展示了我们针对 Django 模式迁移的竞品方案!我的尝试叫做 dmigrations (https://simonwillison.net/2008/Sep/3/dmigrations/),由我与伦敦 Global Radio 的一个团队开发。 Django 的迁移可以从模型定义自动生成,并且支持回滚到先前版本。`sqlite-utils` 的方法故意更简单:与 Django 不同,`sqlite-utils` 鼓励通过编程方式创建表,而非使用模型定义 ORM,因此没有可用于自动生成迁移的基础。 我决定跳过回滚功能,因为根据我的经验,该功能极少被使用。对于 SQLite 项目,一种实现回滚的简单方法是在应用迁移之前创建数据库文件的副本! #### 从 sqlite-migrate 迁移 `sqlite-utils` 迁移的设计已有三年历史了——我最初将其作为单独包 release 为 sqlite-migrate (https://github.com/simonw/sqlite-migrate),但该包从未完全脱离 beta 版本。 我在足够多的地方使用了该包,因此对其设计有信心,于是决定将其提升为 `sqlite-utils` 的一个特性,使其默认对日益增长的 sqlite-utils/Datasette/LLM 生态系统中的所有其他工具可用。 我做了 sqlite-migrate 的 最后一次发布 (https://github.com/simonw/sqlite-migrate/releases/tag/0.2),使其依赖于 `sqlite-utils>=4` 并将 `__init__.py` 文件替换为以下内容: `` from sqlite_utils import Migrations __all__ = ["Migrations"] `` 任何依赖 `sqlite-migrate` 的现有项目都应继续正常工作,无需改动。 #### sqlite-utils 4.0 中的其他所有内容 以下是该版本的发布说明,并附有一些内联注释: > 4.0 版本包含一些次要的向下不兼容修复(因此主版本号提升),并引入了三个主要新特性: - 数据库迁移 (https://sqlite-utils.datasette.io/en/stable/migrations.html#migrations),提供一种结构化机制,用于随时间演变项目的模式。(#752 (https://github.com/simonw/sqlite-utils/issues/752)) 我将迁移视为标志性的新特性,因此写了这篇博客文章。 > - 通过 `db.atomic()` 实现的嵌套事务支持 (https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-atomic),以及库中事务工作方式的众多改进。(#755 (https://github.com/simonw/sqlite-utils/issues/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 支持 保存点 (https://sqlite.org/lang_savepoint.html),因此 `db.atomic()` 可以嵌套使用,实现事务内部的事务。这非常简洁! > - 支持复合外键 (https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-compound-foreign-keys),包括创建、转换以及通过 table.foreign_keys (https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-introspection-foreign-keys) 进行内省。(#594 (https://github.com/simonw/sqlite-utils/issues/594)) 这个特性的出现是因为我让一个编码代理审查所有开放的问题和 PR,看看哪些应该包含在 4.0 版本中(因为如果以后添加,它们将代表破坏性变更),它正确识别出复合外键正是这类特性。 我从对 `table.foreign_keys` (https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-introspection-foreign-keys) 内省方法的破坏性变更开始,然后决定看看 Claude Fable 5 能否处理将复合外键*创建*集成到库中的更繁琐工作。它帮助设计的 API 对我来说 感觉完全正确 (https://sqlite-utils.datasette.io/en/stable/python-api.html#compound-foreign-keys)——与该库其余部分的工作方式一致。 > 其他值得注意的变更包括: - Upsert 现在使用 SQLite 的 `INSERT ... ON CONFLICT ... DO UPDATE SET` 语法,自动检测现有表的主键,并拒绝缺少必需主键值的记录。(#652 (https://github.com/simonw/sqlite-utils/issues/652)) 这是促使我考虑进行破坏性变更的 4.0 版本提升的第一个变更。我构建此功能是为了支持 sqlite-chronicle (https://github.com/simonw/sqlite-chronicle),该功能使用触发器跟踪表中已插入、更新或删除的行。 > - `db.query()` 现在立即执行,并拒绝不返回行的语句;写入和 DDL 请使用 `db.execute()`。 这可能是 最具破坏性的变更 (https://sqlite-utils.datasette.io/en/stable/upgrading.html#python-api-changes)——我不得不在自己的代码中修改几处,将 `db.query()` 切换为 `db.execute()`。 > - 默认情况下,CSV 和 TSV 导入现在会自动检测列类型,而插入到现有表时则会保留这些表的列类型。(#679 (https://github.com/simonw/sqlite-utils/issues/679)) `sqlite-utils insert data.db creatures creatures.csv --detect-types` 标志是后来添加的,用于根据 CSV 中的数据自动检测列类型(text、integer、real)。这应该成为默认行为,而发布 4.0 使我能够做到这一点。 > - `table.extract()` 和 `extracts=` 不再为所有 `null` 值创建查找表记录。(#186 (https://github.com/simonw/sqlite-utils/issues/186)) 该版本解决的最古老问题——底层 bug 是由我(本人)于 2020 年 10 月提交的。 > 关于不兼容变更的详细信息,请参见从 3.x 升级到 4.0 (https://sqlite-utils.datasette.io/en/stable/upgrading.html#upgrading-3-to-4)。有关 4.0 预发布周期内所提供特性和修复的详细发布说明,请参见 4.0a0 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0a0)、4.0a1 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0a1)、4.0rc1 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc1)、4.0rc2 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc2)、4.0rc3 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc3) 和 4.0rc4 (https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc4)。 升级指南完全由 Claude Fable 5、Claude Opus 4.8 和 GPT-5.5 编写。发布说明也是如此。 这是我逐渐放心交给机器人编写的文档类型。它不需要说服任何人什么,也不需要表达任何观点——它的工作是尽可能准确和详细。我已经仔细审查了发布说明,可以确认它们准确且全面。 #### Claude Fable 5 帮了大忙 我发布 sqlite-utils 4.0 的第一个 alpha 版本已是一年多前 (https://sqlite-utils.datasette.io/en/stable/changelog.html#a0-2025-05-08)。我一直拖着稳定版本,因为需要花费大量精力来追踪和清理主版本号允许我承担的许多其他次要设计缺陷。 Claude Fable 5(以及在较小程度上的 Opus 4.8 和 GPT-5.5)的帮助给了我克服惯性所需的动力,并让我能够充分利用花在这个库上的时间。 Fable 在 API 设计方面*品味极佳*,如果你给它更开放的目标,它就会 不断主动推进 (https://simonwillison.net/2026/Jun/11/fable-is-relentlessly-proactive/)。我最成功的提示是针对我以为是最后一个发布候选版的审查任务: > `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 脚本 (https://gist.github.com/simonw/823fdecc031371d56dce39537adc0096),并没有发现特别有趣的东西——其 最终报告在此 (https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4899982463)。 Fable 5 编写了 12 个脚本 (https://gist.github.com/simonw/95800bf584f8e437f1cf0d48d9ef81e6),在其报告中 确定了 4 个发布阻塞问题和 10 个附加问题 (https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4900034150),并构建了一个简洁的 组合复现脚本 (https://gist.githubusercontent.com/simonw/95800bf584f8e437f1cf0d48d9ef81e6/raw/c43918b36a129bba1d2f2a129117aa11c85146c0/12_bug_repros.py),运行后输出如下: `` === 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 (https://github.com/simonw/sqlite-utils/pull/779),我们逐一解决了这些问题。 我毫不怀疑,如果没有最新前沿模型的帮助,sqlite-utils 4.0 的质量远不如现在这样高。

相似文章

sqlite-utils 4.0rc1 新增迁移和嵌套事务

Simon Willison's Blog

sqlite-utils 4.0rc1 是一个候选发布版本,新增了内置数据库迁移(从 sqlite-migrate 移植而来)以及通过 db.atomic() 实现的嵌套事务,同时包含少量不向后兼容的更改。

sqlite-utils 4.0

Simon Willison's Blog

sqlite-utils 4.0 已发布,这是一个用于操作 SQLite 数据库的 Python 库。

sqlite-utils 4.1.1

Simon Willison's Blog

sqlite-utils 4.1.1 修复了在转换带有外键的表时的一个 TransactionError 边界情况,并改进了 CLI 和 Python API 之间的文档交叉引用。

sqlite-utils 4.0rc3

Simon Willison's Blog

sqlite-utils 4.0rc3 是一个候选发布版本,包含新功能,包括复合外键内省和不区分大小写的列匹配。

sqlite-utils 4.1

Simon Willison's Blog

sqlite-utils 4.1 引入了小型功能,包括用于 Python 代码块的 --code、用于列类型覆盖的 --type 以及严格模式支持,继续作为 SQLite CLI 工具发展。