Grist 数据库迁移与 Schema 变更完整指南两库三 Schema 的演进体系【免费下载链接】grist-coreGrist is the evolution of spreadsheets.项目地址: https://gitcode.com/GitHub_Trending/gr/grist-core本文以 documentation/migrations.md 为核心骨架深入讲解 Grist 的数据库迁移体系文档数据库_grist_*与_gristsys_*两套表与 Home 数据库分别由数据引擎Python、Node 服务器TypeScript和 TypeORM 三套代码各自管理、各自演进。读完本文你将掌握何时需要递增SCHEMA_VERSION、如何编写一次migration装饰的 Python 迁移函数、如何在DocStorage.ts中追加基于原生 SQLite 的存储迁移、以及 Home 数据库 TypeORM 迁移文件的生成与命名规范——并能理解只增不改、标记弃用这一迁移哲学背后的跨版本兼容论证。一、总览两类数据库与三套 SchemaGrist 使用两类数据库各自存储不同的数据详见 documentation/database.md数据库职责Home 数据库存储整个 Grist 实例的数据用户与组、计费、组织团队站点、工作区、文档元数据ID、名称、所在工作区、组织/工作区/文档级别的 ACL 权限等文档数据库每个.grist文档就是一个 SQLite 数据库存储表格、页面、视图数据以及文档内部的行/列级 ACL在代码层面这两类数据库由三套数据库 Schema分别管辖每套 Schema 都有自己的变更与迁移方式Schema 位置语言/框架管理对象app/gen-serverTypeORMTypeScriptHome 数据库sandbox/grist/schema.pyPython文档数据库中的_grist_*元数据表app/server/lib/DocStorage.tsTypeScript文档数据库中的_gristsys_*系统表其中_grist_*表由数据引擎Python sandbox管理_gristsys_*表由 Node.js 进程管理。这种一个文档、两种管理者的划分决定了文档数据库内部的迁移也必须分两套机制进行。二、文档数据库的_grist_*表数据引擎的 Schema 迁移2.1 修改 Schema 的前提递增SCHEMA_VERSION当你修改 Grist 的元数据表 Schema即 sandbox/grist/schema.py 中定义的_grist_*表结构时必须做两件事递增文件顶部的SCHEMA_VERSION。当前仓库中该值为46见 sandbox/grist/schema.py。创建一条迁移迁移是一组 DocAction文档动作作用于上一版本的文档使其满足新 Schema。schema.py顶部有明确警告Before changing this file, please review: /documentation/migrations.md即本文档正是修改该文件的必读前置材料。schemaVersion本身也作为元数据存储_grist_DocInfo表中有schemaVersion列类型Int注释说明Version number of the document. It tells us how to migrate it to reach SCHEMA_VERSION见 sandbox/grist/schema.py。迁移的本质就是读取文档当前版本号逐版本补齐到最新。2.2 TypeScript 侧 Schema 的同步TypeScript 在 app/common/schema.ts 中维护着一份 Python Schema 的拷贝只要 Python Schema 变化它就必须同步更新。该文件由core/sandbox/gen_js_schema.py自动生成文件头部注明 THIS FILE IS AUTO-GENERATED其SCHEMA_VERSION同样为46与 Python 侧保持一致。这意味着 Schema 变更不是单点修改而是改 Python 源 → 重新生成 TS 拷贝的联动流程。2.3 编写迁移函数migration装饰器新增迁移就是在 sandbox/grist/migrations.py 中添加一个以新版本号装饰的函数形式如下文档给出的官方模板migration(schema_version11) def migration11(tdset): return tdset.apply_doc_actions([ add_column(_grist_Views_section, embedId, Text), ])这个示例在仓库中真实存在见 sandbox/grist/migrations.py。migrations.py提供了一些辅助函数让迁移更简洁add_column(table_id, col_id, col_type, ...)AddColumn动作的简写L130-L132maybe_add_column(...)仅在列尚不存在时才添加L135-L138next_id(tdset, table_id)取某表下一个可用行 IDL141-L143safe_parse(json_str)解析 JSON非法时返回空对象L146-L150。migration装饰器L112-L127会把迁移函数注册进全局的all_migrations字典键为版本号。它还支持need_all_tablesTrue参数迁移会先用仅元数据表的方式尝试执行若某个迁移函数被标记为需要全表数据则失败后用全表重试。文档明确建议新迁移不应设置need_all_tablesTrue因为它要求对包含 on-demand 表在内的大文档做更多处理代价更高。2.4 迁移的执行机制create_migrations真正编排迁移执行的是create_migrationsL47-L104其核心流程为从_grist_DocInfo.schemaVersion读取文档当前版本读不到则视为 0构造一个TableDataSet来自table_data_set用当前 Schema 的定义重建旧文档中的各表并对旧文档中当前 Schema 已不认识的即已弃用的列使用不完整的默认列定义见 L74-L87将原始数据以BulkAddRecord动作灌入TableDataSet从doc_version 1逐版本执行到SCHEMA_VERSION见 L93-L97缺失的版本使用noop_migration空实现兜底无论是否真的做了迁移最后总是追加一条UpdateRecord(_grist_DocInfo, 1, {schemaVersion: SCHEMA_VERSION})把文档版本号刷新到最新L101-L103。注意若文档版本高于当前SCHEMA_VERSION降级场景唯一的动作就是上面这条版本号更新见 L99-L100 的注释。2.5 数据搬运型迁移不止是加列许多迁移需要在加列的同时搬运或改写数据。仓库中有大量这样的例子例如migration14L559-L598一次性创建整套 ACL 表_grist_ACLMemberships、_grist_ACLPrincipals、_grist_ACLResources、_grist_ACLRules并BulkAddRecord初始化四个默认组、默认资源与默认规则permissions: 0x3F即 63对应 OWNER 全权限migration15L600-L624为_grist_Views_section_field增加filter列后读取所有 section 的filterSpec把每个字段的过滤配置从 section 迁移到字段上同时宣告filterSpec弃用migration16L626-L648新增visibleCol列后遍历所有列与视图字段根据Ref:类型与widgetOptions反推并回填visibleCol的引用值。这类迁移展示了标准姿势先用tdset.all_tables[...]读取旧数据构造新动作序列再tdset.apply_doc_actions统一应用。2.6 迁移的冒烟验证migrations.py文件头部的哲学注释中L30-L36给出了每次迁移后建议执行的验证命令# 用真实样本文档验证升级路径 ./test/upgradeDocument public_samples/*.grist # 跑回归测试更新回归基准数据 UPDATE_REGRESSION_DATA1 GREP_TESTSDocRegressionTests ./test/testrun.sh server # 再验证一个内置 fixture 文档 ./test/upgradeDocument core/test/fixtures/docs/Hello.grist若在 gvisor 沙箱下执行有困难可在命令前加GRIST_SANDBOX_FLAVORunsandboxed前缀绕过沙箱需注意这会让文档在无沙箱环境下运行务必谨慎。三、文档数据库的_gristsys_*表Node 服务器的存储迁移_gristsys_*系统表的 Schema 与迁移都定义在 app/server/lib/DocStorage.ts 中。这些表由 Node.js 服务器管理而非数据引擎——包括_gristsys_Action、_gristsys_Action_step、_gristsys_ActionHistory、_gristsys_ActionHistoryBranch、_gristsys_FileInfo、_gristsys_PluginData等见 L84-L143 的建表 SQL。3.1 两套版本号Storage Version 与 Schema VersionDocStorage.ts的注释L76-L80澄清了两个易混淆的概念Storage Version存储版本即docStorageSchema.migrations数组的长度跟踪磁盘存储方式以及非数据引擎表_gristsys_*的 Schema 变化Schema VersionSchema 版本即数据引擎元数据的版本上一节的SCHEMA_VERSION。在 SQLite 中存储版本号通过PRAGMA user_version持久化保存。3.2 迁移实现原始 SQLite 查询函数与 Python 侧的动作序列不同_gristsys_*的迁移是直接对数据库执行原生 SQLite 查询的函数数组。以 v1→v2 的迁移为例L150-L169它把所有表的列类型统一改为 BLOB采用了标准的重建手法CREATE TABLE 临时表 (按新定义生成列); INSERT INTO 临时表 SELECT 所有列 FROM 旧表; DROP TABLE 旧表; ALTER TABLE 临时表 RENAME TO 旧表;后续版本依次处理v3 将旧_gristsys_Action*表转换为_gristsys_ActionHistory*L220-L234以及补建_gristsys_Files表、为_gristsys_Action补linkId列L192-L199等历史演进。由于这些迁移直接面对磁盘布局涉及 DDL/DML 级别的 SQL 操作因此对 SQLite 方言的细节如quoteIdent标识符转义、PRAGMA table_info反射要求更高。四、迁移哲学为什么要用笨实现加载旧文档文档明确指出迁移很棘手Migrations are tricky。平时我们思考的是正在编写的软件但迁移面对的却是由旧版本软件创建的文档——它们可能不具备你假设的新逻辑也可能具备当前版本完全不知道的旧逻辑。这正是迁移代码必须使用笨实现dumb implementation来加载与检查数据的原因参见 sandbox/grist/table_data_set.py用主代码库直接加载旧文档通常会失败因为旧文档不满足当前代码的种种隐含假设。TableDataSet提供的是最朴素的表 行 列值容器配合create_migrations中对未知列使用不完整默认定义的策略才能容忍旧文档的任意历史形态。五、限制与规则保证跨版本共享的可能文档给出了迁移必须遵守的硬性规则这是整份规范的核心约束WARNING: Do not remove, modify, or rename metadata tables or columns.不要删除、修改或重命名元数据表/列。5.1 弃用而非删除注释约定旧列与旧表应当用注释标记为弃用而不是删除。注释格式为# columnName is deprecated as of version XX. Do not remove or reuse.仓库中大量真实注释遵循此约定例如_grist_DocInfo.docId、peerssandbox/grist/schema.py、_grist_Imports整表L102等。标记弃用有两个目的防止未来添加同名实体或防止用不同含义复用同一列。目前仅靠注释 在代码中移除对该实体的引用未来可能增加代码层面的标记机制来强制禁止使用。5.2 为什么不能删跨版本通信论证文档用一个 A/B 双版本场景说明保留旧列的正当性假设 A 在 v10、B 在 v11。若列foo存在于 v10 却在 v11 被删除那么 A 可能发送引用foo的动作而 B 的代码完全不认识foo会判定这些动作非法。解决办法就是 B 必须仍然知道foo——所以我们不删除旧列。同样的论证适用于重命名列以及修改列例如改变类型。5.3 改变含义或类型必须新建列WARNING: If you change the meaning or type of a column, you have to create a new column with a new name.如果你改变了某列的含义或类型必须用新名字创建新列并编写迁移从旧列填充新列同时把旧列标记为弃用。这条规则的推理基础与上一节一致列被改造后旧版本发出的、针对旧语义的动作就会失效。综合来看这套规则的目标是让不同版本的用户之间勉强可能共享文档即使仍需要更多工作并让升级后用旧版本打开文档这件事相对安全。六、Home 数据库TypeORM 迁移Home 数据库的迁移由TypeORM负责。迁移文件位于 app/gen-server/migration在启动时自动执行——因此部署者无需手动运行任何迁移命令。6.1 迁移文件形态与命名该目录下共有 50 个迁移文件均以毫秒级 Epoch 时间戳 语义化名称命名例如1536634251710-Initial.ts、1551805156919-LoginDisplayEmail.ts、1759256005608-Proposals.ts、1764872085347-OAuthClientsAndGrants.ts等。从命名即可看出 Home 数据库的演进足迹登录、计费、别名、团队、文档回收站、共享、配置、服务账号、提案Proposals、OAuth/OIDC 授权等能力都是通过逐个迁移叠加出来的。6.2 生成新迁移目录中的 README.md 给出了标准的生成流程# 在 app/gen-server/migration 目录下执行 npx typeorm migration:create MigrationName生成后需要把新迁移登记到 test/gen-server/migrations.ts确保测试环境也能识别。6.3 rebase 后的重命名约定由于迁移文件以时间戳为前缀而 rebase 可能打乱顺序README 还约定正在开发的迁移必须是最新的那一个。获取当前毫秒级时间戳的命令date %s%N | cut -b1-13拿到新时间戳后将其作为你开发中的迁移文件前缀即可保证它排在其他迁移之后。七、总结三套机制的适用边界速查场景修改位置迁移机制修改_grist_*元数据表数据引擎sandbox/grist/schema.py 同步 app/common/schema.ts递增SCHEMA_VERSION在 sandbox/grist/migrations.py 新增migration函数返回 DocAction 序列修改_gristsys_*系统表Node 进程app/server/lib/DocStorage.ts向docStorageSchema.migrations数组追加函数执行原生 SQLite 查询版本号 数组长度存于PRAGMA user_version修改 Home 数据库app/gen-server/migrationTypeORM 迁移文件启动时自动运行无论走哪条通道都要遵守共同铁律不删、不改、不重命名元数据表/列改变语义必须新建列旧实体用# name is deprecated as of version XX. Do not remove or reuse.注释标记弃用。这套纪律让 Grist 的文档可以在不同版本的用户之间流转也让升级后回退成为可能——这正是 Grist 作为电子表格的进化体所依赖的数据兼容性根基。【免费下载链接】grist-coreGrist is the evolution of spreadsheets.项目地址: https://gitcode.com/GitHub_Trending/gr/grist-core创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考