
Cherry Studio 数据库层深度解析Drizzle Schema 设计、迁移管线与 SQLite 工程实践【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studioCherry Studio 的桌面端使用 SQLite Drizzle ORM 作为本地数据存储方案其数据库层位于 src/main/data/db 目录。本文围绕该目录的设计体系展开从 schema 文件的组织与命名约定、列助手Column Helpers、迁移生成与执行管线到 Drizzle 无法管理的 FTS5 虚拟表与触发器CUSTOM_SQL_STATEMENTS再到 SQLite 约束错误到DataApiError的翻译层系统讲解这套数据库层如何兼顾类型安全、启动性能与数据一致性。读完本文你将掌握 Cherry Studio 数据库层的完整架构脉络以及在其上新增表、生成迁移、编写 schema、处理约束错误的全部实战方法。一、目录结构与模块职责src/main/data/db是 Cherry Studio 数据库层的中枢其设计遵循schema 定义、迁移执行、连接管理、数据播种、备份恢复的分层原则src/main/data/db/ ├── schemas/ # Drizzle 表定义 │ ├── _columnHelpers.ts # 可复用的列定义UUID 主键、时间戳等 │ ├── topic.ts # Topic 表 │ ├── message.ts # Message 表 MESSAGE_FTS_STATEMENTSFTS5 虚拟表与触发器 │ └── ... # 其余表agent、file、knowledge、mcpServer 等约 30 张表 ├── seeding/ # 数据播种详见 seeding/README.md ├── restore/ # 备份恢复提升原语详见 restore/README.md ├── applyMigrations.ts # 共享迁移路径drizzle migrate 自定义 SQL 回放 ├── customSqls.ts # 自定义 SQL触发器、虚拟表——每次启动回放 ├── sqliteErrors.ts # SQLite 约束错误 → DataApiError 翻译层 └── DbService.ts # 数据库连接管理从源码结构看DbService.ts是连接管理的唯一入口通过生命周期系统注册为DbService服务Injectable(DbService)其核心职责包括连接初始化better-sqlite3打开单条持久连接并在构造函数中调用ensureDatabaseIntegrity()清理损坏的数据库文件PRAGMA 配置configurePragmas()一次性设置journal_modeWAL、synchronousNORMAL、foreign_keysON、busy_timeout5000迁移与播种onInit()中按固定顺序执行configurePragmas()→migrateDb()→SeedRunner.runAll(seeders)写事务withWriteTx(fn)提供BEGIN IMMEDIATE同步事务包装备份恢复支持createSnapshot()VACUUM INTO快照与checkpointTruncate()WAL 检查点截断。二、命名约定一眼可读的 Schema 规范表名与导出名db/README.md明确了三条命名规则维度规范示例表名单数 snake_casetopic、message、app_state导出名xxxTable模式topicTable、messageTable推断行类型XxxRow$inferSelect/InsertXxxRow$inferInsertMcpServerRow、InsertMcpServerRowRow后缀的意义在于将数据库行类型与 API 层的XxxEntity类型区分开避免二者在服务层混用。这条约定在 docs/references/architecture/naming-conventions.md#53-drizzle-schema-inferred-row-types 中有完整阐述。Schema 文件组织docs/references/data/database-patterns.md 补充了文件组织原则强关联的同域表→ 合并到同一文件如tagging.ts同时定义tagTable与entityTagTable核心表 / 复杂业务逻辑表→ 每表一文件如message.ts、topic.ts工具类辅助定义→ 使用下划线前缀如_columnHelpers.ts表示非表定义。这种组织方式让 schema 文件天然成为数据库词典打开schemas/目录即可概览整个数据模型。三、迁移工作流从生成到执行常用命令修改 schema 后使用以下命令生成迁移命令真实定义于 package.json 的 scripts 中# 生成迁移schema 变更后 pnpm db:migrations:generate # 校验迁移链完整性 pnpm db:migrations:check两条命令的职责分工是generate执行drizzle-kit generate对比 schema 差异生成新的.sql迁移文件与 snapshotcheck执行drizzle-kit check校验迁移链是否断裂或分叉。迁移链是 git 跟踪的产物迁移产物位于 migrations/sqlite-drizzle包含三部分*.sql—— 实际的迁移 SQLmeta/_journal.json—— 有序索引meta/*_snapshot.json—— 每步迁移的 schema 快照。改动 schema 文件后必须重新生成并提交这些产物CI 会通过git status --porcelain migrations/检查未提交的迁移文件未提交即 CI 失败。drizzle-kit 配置要点migrations/sqlite-drizzle.config.ts 中的关键配置配置项值说明out./migrations/sqlite-drizzle迁移产物输出目录schema glob./src/main/data/db/schemas/**/!(*.test).ts递归扫描排除*.test.ts避免 drizzle-kit 加载 vitest 文件dialectsqliteSQLite 方言casingsnake_caseTS 属性ftsRowid映射为 DB 列fts_rowid重新生成绝不重命名当合并/变基与上游迁移冲突时正确的处理方式是删除本地冲突的.sql与其meta/*_snapshot.json然后重新运行pnpm db:migrations:generate。绝不要手动重命名/重编号.sql或手改 snapshot —— 那会复用 snapshot 的随机id、分叉迁移链并导致所有人的generate中止。⚠️ 需要特别警惕drizzle-kit generate即使遇到分叉的迁移链也会以退出码 0 成功所以它永远不能作为完整性检查手段。只有pnpm db:migrations:check能检测重复/分叉的迁移链。CI 会同时运行两者链完整性检查 generate 后 diff 的漂移门禁而本地pnpm lint/pnpm test/pnpm build:check均不运行这两项 —— 这意味着 chain fork 和 schema↔migration 漂移在本地不可见推送前必须重新生成并提交。四、启动期的数据库构建顺序DbService.onInit()按固定顺序构建数据库其执行流程在 database-construction.md 中有详细表格说明#步骤说明1ensureDatabaseIntegrity()构造函数删除 0 字节的.db文件和孤立-wal/-shm副文件避免SQLITE_IOERR_SHORT_READ。打开数据库本身可能删除文件2configurePragmas()journal_modeWALdb.run()设置持久化于文件仅一次synchronousNORMALforeign_keysON在唯一持久连接上只设置一次3applyMigrations()migrate()应用migrations/sqlite-drizzle/中未应用的 drizzle 迁移4applyMigrations()自定义 SQL 回放每次启动无条件回放CUSTOM_SQL_STATEMENTSFTS 虚拟表 触发器5SeedRunner.runAll(seeders)在刚迁移完成的 schema 上运行seeder 依赖的 schema 变更必须先落在迁移里单条持久连接PRAGMA 只需设置一次better-sqlite3在整个进程生命周期内保持一条连接DbService.ts 中的注释明确说明了这一点因此synchronousNORMAL、foreign_keysON这类 per-connection PRAGMA 只需在configurePragmas()中设置一次即可全程生效 —— 不存在事务边界重连导致 PRAGMA 重置的问题也不需要任何逐语句的回放机制。WAL同样只设置一次并持久化在数据库文件中。applyMigrations三段式迁移路径applyMigrations.ts 是整个数据库的共享迁移函数被三个调用方复用DbService.onInit()在线库、测试夹具一次性库、备份恢复管线分离的 work.sqlite 前向迁移。其执行分三段关闭外键约束后执行 drizzle migrateSQLite 无法原地修改表约束所以 drizzle-kit 会把约束/列类型变更编译成表重建CREATE __new_x → INSERT SELECT → DROP x → RENAME并在其内部使用PRAGMA foreign_keysOFF保护。但 drizzle-orm 的 migrator 会把每条语句包装进同一个事务而 SQLite 规定该 PRAGMA 在事务内是 no-op —— 因此这里必须在 migrator 事务之外手动关闭外键约束否则重建过程中的DROP会触发每个子表的ON DELETE动作并静默带走它们的行恢复外键约束并检查悬挂引用迁移完成后重新开启外键约束若此前是开启状态则执行PRAGMA foreign_key_check发现迁移自身引入的悬挂外键引用时记录错误日志不阻断启动因为该路径也服务于恢复与测试硬失败对用户不可恢复但绝不允许无声通过回放自定义 SQL逐条执行CUSTOM_SQL_STATEMENTS中的语句。打包应用的迁移路径打包后的应用中migrate()通过application.getPath(app.database.migrations)读取extraResources/migrations/sqlite-drizzle开发环境下则使用相对路径。如果迁移文件夹未通过 electron-builder 的extraResources打包进应用开发环境正常、打包版启动时必失败。五、自定义 SQLDrizzle 管不到的触发器与虚拟表为什么需要CUSTOM_SQL_STATEMENTSDrizzle ORM 不跟踪三类 SQL 对象虚拟表FTS5、触发器、带表达式的自定义索引。因此它们不在任何.sql迁移文件中而是以string[]形式存在于 schema 文件如 schemas/message.ts 中的MESSAGE_FTS_STATEMENTS、schemas/agentSessionMessage.ts 中的AGENT_SESSION_MESSAGE_FTS_STATEMENTS由 customSqls.ts 聚合为CUSTOM_SQL_STATEMENTS并在每次启动时由applyMigrations()在migrate()之后回放。这一设计是必须的表重建的DROP TABLE会静默删除该表上的所有触发器因此每次启动都必须重新断言触发器存在 —— 这正好在同一启动内完成自愈机制。成本O(1) 元数据操作约 0.1ms可能有人会问为什么不在确实有迁移执行时才回放自定义 SQL答案在 database-construction.md 中给出的实测数据重放整套 FTS 自定义 SQL 仅需约 0.1ms且与行数无关better-sqlite3 实测空库 0.11ms5 万行 0.13ms。因为它是纯元数据操作 ——CREATE VIRTUAL TABLE IF NOT EXISTS已存在则跳过DROP/CREATE TRIGGER仅触碰sqlite_master不触碰任何数据行、不重新分词、不重建索引。更关键的是正确性触发器/虚拟表定义存放在这里而非迁移文件中意味着发布版本可以在没有 schema 迁移的情况下修改触发器体例如可搜索文本提取逻辑或fts_rowid接线方式每次启动的重新断言正是让这种变更在既有数据库上生效的机制。真正决定是否重新运行的条件是定义是否变了或重建是否删掉了它而非是否跑了迁移廉价的每次无条件断言恰好覆盖了两种情况而无需检测任何一种。两类 bucket工作该放哪里Bucket示例位置成本幂等的 schema 对象重断言FTS 虚拟表、触发器CUSTOM_SQL_STATEMENTS——每次启动O(1) 元数据一次性数据操作backfill、FTSrebuild、重新分词有日志的一次性迁移 ——绝不每次启动O(N)把 O(N) 类操作放进CUSTOM_SQL_STATEMENTS是致命的启动时执行的 backfill 会在每次启动时重复运行 O(N) 开销。幂等性规则CUSTOM_SQL_STATEMENTS数组每次启动都会重放非事务性、每条语句一次db.run而DbService是 fail-fast 的 —— 非幂等语句会在第二次启动时抛错并中止启动。语句顺序也很重要CREATE TRIGGER必须位于其引用的CREATE VIRTUAL TABLE之后。两条硬性规则虚拟表→CREATE VIRTUAL TABLE IF NOT EXISTS跨启动存活触发器→DROP TRIGGER IF EXISTS name 裸CREATE TRIGGER不能用IF NOT EXISTS这样编辑过的触发器体会真正替换旧体。如果对触发器用IF NOT EXISTS过期的触发器体会被永远冻结。六、FTS5 外部内容表与fts_rowid稳定性规则核心规则永远不要依赖隐式rowid两张聊天搜索表message_fts、agent_session_message_fts都是 FTS5 external-content 表这里正是fts_rowid规则的权威出处。表重建drizzle 的INSERT…SELECT会丢弃隐式 rowid以及VACUUM都会重排基表的隐式rowid。如果 external-content FTS5 表使用content_rowidrowid索引会保留旧 rowid然后静默指向错误的行 —— 表现为搜索结果错误或缺失且不抛出任何错误。解决方案定义真实的integer().unique()列fts_rowid配置content_rowidfts_rowid由 AFTER INSERT 触发器赋值。因为fts_rowid是真实列drizzle 的表重建会原样复制它VACUUM也不会移动它 —— 索引构造性地保持对齐。验证只有integrity-check, 1可靠默认的INSERT INTO fts(fts) VALUES(integrity-check)不会将索引与内容表做比较rowid 失步会静默通过。必须使用INSERT INTO fts(fts, rank) VALUES(integrity-check, 1)回归防护位于 src/main/data/db/tests/ftsRebuild.test.ts该测试复现了 rowid 重排的重建场景并断言integrity-check, 1保持干净同时断言 NULLfts_rowid会使其抛错。fts_rowid的属性属性细节设计上可空AFTER INSERT 触发器在行存在之后才填充它NOT NULL列会在触发器运行前拒绝该行赋值方式AFTER INSERT 触发器中fts_rowid (SELECT COALESCE(MAX(fts_rowid),0)1 FROM table)。…_fts_rowid_uniqUNIQUE 索引使其成为 O(log N) 的 min/max 查找裸列会导致批量迁移 O(N²)并能响亮地拒绝任何重复值。仅因DbService.withWriteTx的写序列化而无竞态本地唯一物理身份与rowid类似应用代码绝不设置、备份中绝不导出/导入。恢复必须逐行通过触发器插入内容行若遗留 NULLfts_rowid会导致integrity-check, 1失败且该行不可搜索searchable_text触发器填充不是 SQLiteGENERATED列。对文本片段做group_concat并用COALESCE(…,)包裹纯工具/空消息时group_concat返回 NULL该列是NOT NULL DEFAULT 。message表提取text片段 data-code/data-translation/data-compact内容 data-error消息agent_session_message仅提取text片段防止隐藏的推理内容通过搜索摘要泄露。新增可搜索片段类型意味着更新相应触发器表达式 —— 由于触发器是 DROPCREATE修复会在下一次启动回放时落地到既有数据库从 schemas/message.ts 的MESSAGE_FTS_STATEMENTS可以完整看到这套机制的落地CREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts5(searchable_text, contentmessage, content_rowidfts_rowid, tokenizetrigram)定义了 trigram 分词的外部内容表message_aiAFTER INSERT、message_adAFTER DELETE、message_auAFTER UPDATE OF data三个触发器分别负责赋值fts_rowid、填充searchable_text并同步 FTS 索引。知识库的search_text_fts遵循同一规则src/main/features/knowledge/pipeline/vectorstore/indexStore/schema.ts 中的search_text_fts同样以稳定fts_rowid列作为键由search_text_ai触发器赋值content_rowidfts_rowid。它是每个知识库独立的index.sqlite不在主库、不受 drizzle 管理、不在CUSTOM_SQL_STATEMENTS中但同样的风险适用其reclaim()路径在大规模删除后运行VACUUM归还空闲页给操作系统这会重排隐式 rowid —— 键在fts_rowid上使 external-content 索引构造性保持对齐。七、列助手Column Helpers统一的主键与时间戳所有辅助函数都从 schemas/_columnHelpers.ts 导出schema 文件中通过展开运算符使用import { uuidPrimaryKey, createUpdateTimestamps } from ./_columnHelpers export const myTable sqliteTable(my_table, { id: uuidPrimaryKey(), name: text(), ...createUpdateTimestamps })主键助手助手UUID 版本适用场景uuidPrimaryKey()v4随机通用表uuidPrimaryKeyOrdered()v7时间有序大数据量、基于时间的查询频繁的表如messageTable行为特性插入时未提供 id 则自动生成迁移场景可手动指定插入后使用.returning()获取生成的 id。时间戳助手助手字段适用场景createUpdateTimestampscreatedAt、updatedAt无软删除的表createUpdateDeleteTimestampscreatedAt、updatedAt、deletedAt有软删除的表行为特性createdAt插入时自动设为Date.now()updatedAt插入时设置、更新时自动更新$onUpdateFndeletedAt默认null软删除时置为时间戳。三个时间戳字段在 DB 层面都是NOT NULL应用代码在.values({...})中可以省略它们。辅助列扩展_columnHelpers.ts还提供排序相关助手orderKeyColumns分数索引字符串列order_key、orderKeyIndexorder_key索引、scopedOrderKeyIndex(scope, order_key)复合索引如user_model_provider_id_order_key_idx。八、错误翻译SQLite 约束 → DataApiErrorsrc/main/data/db/sqliteErrors.ts 将 Drizzle 抛出的 SQLite 约束违规翻译为DataApiError映射关系为UNIQUE → 409、FK → 404、CHECK / NOT NULL → 422。它对外暴露三个 API1.classifySqliteError(e)—— 分类遍历.cause链最大深度 5 层防止循环 cause 图返回描述违规类型的可辨识联合discriminated uniontype SqliteConstraint | { kind: unique; columns: string[] } | { kind: foreign_key } | { kind: check; constraintName?: string } | { kind: not_null; columns: string[] }非约束错误返回null。分类的权威信号是 SQLite 扩展错误码SQLITE_CONSTRAINT_UNIQUE等同时保留了消息子串回退用于未传播.code的包装器触发时记录警告日志使驱动漂移可见。注意SQLITE_CONSTRAINT_PRIMARYKEY和SQLITE_CONSTRAINT_ROWID在语义上属于 UNIQUE 违规会一并归入unique分支。2.withSqliteErrors(op, handlers)—— 执行与路由运行op将识别出的违规路由到匹配的 handler未注册 handler 的约束类型以及非 SQLite 错误在结构上必然原样重抛—— 这是该模块的类型层设计价值忘记重抛这类 bug 在客户端代码中不可表达。三个结构性保证匹配到约束 有匹配 handler → 抛出 handler 构造的DataApiError匹配到约束但无匹配 handler → 原样重抛原始错误保留引用、堆栈和.cause链非 SQLite 错误 → 原样重抛。3.defaultHandlersFor(resource, identifier)—— CRUD 默认 handler 集为常见 CRUD 场景提供完整默认集约束类型默认映射UNIQUEconflict(resource identifier already exists)FOREIGN KEYnotFound(resource, identifier)——插入语义此操作引用的父行不存在。若执行的是可能因ON DELETE RESTRICT失败的删除相反语义仍被子行引用需用invalidOperation覆盖foreignKeyCHECK在_root字段上的validation含约束名NOT NULLvalidation每个违规列映射为字段级错误典型用法是通过展开覆盖特定 kindawait withSqliteErrors(op, { ...defaultHandlersFor(Tag, id), foreignKey: () DataApiErrorFactory.invalidOperation( Cannot delete Tag ${id}: still referenced ), } satisfies SqliteErrorHandlers)使用纪律TOCTOU 回退而非预校验的替代品withSqliteErrors的 handler 是TOCTOU / 并发回退不是应用层预校验的替代品。任何 UNIQUE / FK 约束的业务主路径都应有显式的assertXxxAvailable预查询。defaultHandlersFor的默认消息刻意简短 —— 它们用于浮出有东西与我们竞争了这一信号而非作为一等公民的用户消息。把 SQLite 约束错误当作真实校验替代品的服务代码会逐渐丢失预检查纪律。使用时应给 handlers 字面量追加satisfies SqliteErrorHandlers让 TypeScript 编译器捕获拼写错误如unqiue导致的静默 fall-through。九、写入序列化DbService.withWriteTxapplication.get(DbService).withWriteTx(fn)在唯一持久连接上将fn作为一个同步BEGIN IMMEDIATE事务运行。何时值得用事务better-sqlite3中每条语句自身都是原子的 —— 单独的getDb().insert(...).run()是一个完整的隐式事务不需要包装。事务只有在跨多条语句的原子提交时才物有所值应该用组合多个写入或读后写校验/查询后插入/更新/删除为单一原子单元时 —— 项目中大多数写路径都是如此同时触及连接表的 create/update/delete、清理 pin/tag、通过邻居读取重排、级联删除。前提是原子性跨语句回滚而非序列化 —— 单条同步连接在构造上已经序列化了所有写入不该用单条自动提交写入 —— 直接调用getDb()或将getDb()传给写方法的*Tx形式如this.fooTx(getDb(), …)。单写走withWriteTx对原子性毫无增益还会虚假暗示存在多语句不变量。withWriteTx与db.transaction()的关系withWriteTx(fn)是getDb().transaction(fn, { behavior: immediate })在isReady守卫后的薄包装。BEGIN IMMEDIATE提前获取写锁仅当第二个连接并发写入时才有关键意义主库只有一条连接因此它与普通db.transaction(fn)行为一致。项目约定优先使用withWriteTx作为可 grep 的写接缝与正确的写意图默认值 —— 但直接db.transaction()在原子性上等价并非错误。fn必须是同步的且只能做 DB 操作无网络、文件 IO、handler 执行—— better-sqlite3 拒绝返回 Promise 的事务回调事务会阻塞唯一连接直到fn返回。调用规则速查规则理由fn必须同步、仅做 DB 操作better-sqlite3 拒绝 Promise 回调事务阻塞唯一连接不要包装读操作WAL 模式给读者快照隔离包装只会增加无谓的序列化不要包装单条自动提交写一条语句已是隐式事务 —— 直接调getDb()紧循环整体包进一个withWriteTx一个BEGIN IMMEDIATE事务 vs N 个双形态 DAO 模式每个写方法都有可组合的*Tx形式和薄的非 Tx 包装单写方法的包装将getDb()传给*Tx形式多写/读后写方法的包装在单个withWriteTx内组合一个或多个*Tx调用。*Tx形式还能跨服务组合如JobManager.enqueueTx(tx, type, input)搭调用方的事务便车使业务状态写入与其任务入队整体提交。JobService/JobScheduleService是规范示例。十、测试与验证真实生产路径的回归防护数据库测试通过setupTestDatabase()在真实 better-sqlite3 连接上运行真实的生产迁移 CUSTOM_SQL_STATEMENTS因此测试 schema 与生产逐字节一致 —— 测试中手写CREATE TABLE是被禁止的。原始 SQL / PRAGMA / FTSMATCH通过句柄的裸连接dbh.sqlite执行。src/main/data/db/__tests__/下的测试覆盖了关键路径ftsRebuild.test.ts—— FTS rowid 重排重建回归sqliteErrors.test.ts—— 错误翻译分类与重抛语义applyMigrations.test.ts/applyMigrations.populated.test.ts—— 迁移路径含外键关闭/恢复逻辑withWriteTx.test.ts—— 写事务agentSessionMessageReasoningBackfill.test.ts—— 一次性 backfill 迁移DbService.restoreApis.test.ts—— 快照与检查点恢复 API。原生模块 ABI 注意事项better-sqlite3 不是 N-API 模块与仓库其他原生依赖不同其构建是 ABI 特定的。测试在系统 Node 下运行模块保持Node ABIpnpm install默认mainVitest 项目的pretest:main钩子在pnpm test:main前运行pnpm rebuild:node保证这一点。pnpm dev和打包则切换为Electron ABIpnpm rebuild:electron--force。若在pnpm dev之后直接使用pnpm test:watch或 IDE 的 Vitest需先运行pnpm rebuild:node。十一、常见陷阱速查表陷阱一句话要点自定义 SQL 不在任何.sql中FTS 虚拟表/触发器位于 TScustomSqls.ts且每次启动重跑重建的DROP TABLE会删触发器generate对分叉链也退出 0只有db:migrations:check能捕获CI 两者都跑本地 lint/test 都不跑重新生成绝不重命名删除.sql snapshot 后重新 generate重命名会分叉迁移链提交生成的产物CI 检查git status --porcelain migrations/重新生成后不提交即 CI 失败增列 ≠ 重建CHECK/FK/PK/DEFAULT/NOT-NULL 变更会强制整表重建且不回填既有行DBDEFAULT近乎永久产品可选值优先用服务层?? DEFAULT触发器 DROPCREATE虚拟表 IF NOT EXISTS触发器用IF NOT EXISTS会冻结过期体FTS 键在fts_rowid而非rowid隐式 rowid 在重建/VACUUM 时重排 → 静默失步默认integrity-check不可靠external-content FTS 必须用integrity-check, 1fts_rowid仅限本地绝不在备份中导出恢复必须经触发器插入多语句写入使用事务withWriteTx是约定包装直接db.transaction()等价每条语句作为一个同步事务单连接构造性序列化写入打包迁移需要extraResources开发环境正常、打包版若未随包必失败PRAGMA 在一条连接上只设一次better-sqlite3 保持单条持久连接synchronous/foreign_keys启动时设置后永不回退十二、进阶阅读路径本文是数据库层的总览入口进一步深入可阅读以下仓库文档docs/references/data/database-construction.md —— 构建、迁移、自定义 SQL、FTS5 的完整展开含 boot 顺序表、bucket 划分、幂等规则与 gotchasdocs/references/data/database-patterns.md —— schema 编写模式文件组织、JSON 字段、可空性、外键含自引用与循环引用处理、递归 CTE ORM 双步查询、withWriteTx双形态 DAOdocs/references/data/database-seeding-guide.md —— 初始数据播种默认偏好、内置语言、预设 providerdocs/references/data/best-practice-default-values-and-nullability.md —— 默认值与可空性决策树docs/references/testing/database-testing.md —— 数据库测试夹具与 ABI 注意事项docs/references/architecture/naming-conventions.md —— 全仓库命名约定含 §5.3 Drizzle 推断行类型。这套数据库层设计的关键收益在于Drizzle 的 schema 定义 类型推断提供了全程类型安全每次启动约 0.1ms 的自定义 SQL 重放换来了触发器/虚拟表定义的热更新能力fts_rowid规则从构造上消除了 FTS 索引静默失步的隐患而sqliteErrors翻译层则让约束错误以类型安全的方式映射为 API 层语义。理解这些设计无论是为 Cherry Studio 贡献新功能还是在自己的 Electron SQLite 项目中借鉴工程实践都极具参考价值。【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考