十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Civitai 数据库 Schema 契约包 `@civitai/db-schema`:单一事实来源、类型分发与 Schema 漂移检测实战

Civitai 数据库 Schema 契约包 `@civitai/db-schema`:单一事实来源、类型分发与 Schema 漂移检测实战 Civitai 数据库 Schema 契约包civitai/db-schema单一事实来源、类型分发与 Schema 漂移检测实战【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文是 Civitai 仓库中civitai/db-schema包的技术指南。该包是整个多应用仓库的schema contract模式契约层它集中承载由 Prisma 生成的客户端与类型、由 prisma-kysely 生成的完整DB类型并附带一个能对照真实数据库pg_catalog检测声明与实现漂移的只读检查器。读完本文你将掌握该包的四个导出入口的用途与取舍、如何在 Next.js/Vite 应用中正确接入并完成生成流程以及如何用 drift 检测器与drift:gate门禁持续审计 Prisma schema 与实际数据库之间的一致性。为什么需要独立的 Schema 契约包在 packages/civitai-db-schema/README.md 的开篇这个包被明确定义为schema contract package每个其他包和应用都针对它来做类型声明而不是直接导入prisma/client从而让 schema 拥有单一事实来源single source of truth。这与仓库的 monorepo 结构直接相关apps/下有 auth、creator-studio、moderator、notifications、storage 等多个应用packages/下有 civitai-db、civitai-db-queries、civitai-buzz 等十余个共享包。若每个模块都直接import { PrismaClient } from prisma/clientschema 一旦演进所有引用点都要跟着散落更新极易产生不一致。civitai/db-schema把生成的 Prisma 客户端 所有模型类型集中为唯一出口其他模块一律经由它取类型Prisma 路径运行时拿到Prisma、PrismaClientKysely 路径类型层面拿到完整的DBschema 类型纯类型无运行时开销枚举与模型子路径按需取生成的枚举对象与模型接口。从 src/index.ts 可以看到入口实现的两个关键决策// eslint-disable-next-line import/no-extraneous-dependencies export { Prisma, PrismaClient } from prisma/client; // eslint-disable-next-line import/no-extraneous-dependencies export type * from prisma/client;prisma/client刻意不作为本包的显式依赖声明。源码注释解释了原因一旦声明prisma generate会在 workspace 根目录尝试自动pnpm add prisma/client并失败目前依赖根目录的 hoisting 解析根治方案是让自定义 generator 输出到../generated/client后从./generated再导出。刻意不用export *导出 CommonJS 形态的prisma/client。Turbopack 无法通过运行时export *静态枚举一个 CJS 模块的导出会对每次导入报警告。因此运行时只显式再导出Prisma与PrismaClient真正有运行时代码的部分其余全部用export type *走类型通道——零运行时代码同样的对外表面积没有告警。包结构与四个导出入口包的 manifest package.json 通过exports字段定义了四个子路径出口导入路径提供内容源码位置civitai/db-schemaPrisma、PrismaClient及全部模型类型从prisma/client再导出src/index.tscivitai/db-schema/kyselyDB—— 完整的 Kysely schema 类型纯类型无运行时src/kysely/types.tscivitai/db-schema/enumsPrisma 枚举值 类型src/enums.tscivitai/db-schema/models模型类型src/models.ts另外还暴露了两个内部子路径/kysely/updated-at-tables与/schema-drift。主入口与三个子路径的分工index.ts的注释明确说明生成的枚举对象与模型接口刻意不在此处合并导出它们的名称相互重叠而是要求通过子路径分别导入import { ... } from civitai/db-schema/enums; import type { ... } from civitai/db-schema/models;/enums入口 src/enums.ts 由自定义 Prisma generator 生成头部注明do not edit manually。其形态是as const对象 派生类型例如export const ModelType { Checkpoint: Checkpoint, TextualInversion: TextualInversion, LORA: LORA, Controlnet: Controlnet, VAE: VAE, LLM: LLM, // ... } as const; export type ModelType (typeof ModelType)[keyof typeof ModelType];这样既可在运行时用ModelType.Checkpoint作为值也能在类型层面约束字段。文件涵盖 120 个枚举从模型类型、内容审核NsfwLevel、ReportReason、TagSource到支付Currency、PaymentProvider、CashWithdrawalStatus、漫画/挑战等新业务域与 prisma/schema.full.prisma 一一对应。/models入口 src/models.ts 由prisma-generator-typescript-interfaces生成提供每个表的接口形态含关系字段例如User接口承载了数十个一对多关系。/kysely入口 src/kysely/types.ts 则面向 Kysely 查询构建器定义GeneratedT与Timestamp工具类型后为每张表生成{ id: Generatednumber; ... }形态的表类型并内联导入全部枚举类型——它只用于createKyselyClientsDB()这类类型参数不产生任何运行时字节。在应用中使用该包1. 添加依赖在目标应用的package.json中加入 workspace 依赖// package.json civitai/db-schema: workspace:*2. 与消费者一起转译由于包内源码是 TypeScript 而非编译产物需要与其主要消费方civitai/db一起被宿主转译Next.js在next.config中配置transpilePackagesVite在ssr.noExternal中列出该包。3. 导入import type { DB } from civitai/db-schema/kysely; // 供 createKyselyClientsDB() 使用 import { Prisma, PrismaClient } from civitai/db-schema; // Prisma 路径README 特别提醒通常不需要直接添加本包——它会作为civitai/db的伴随依赖peer被带入只有当你自己需要导入DB类型或枚举时才显式声明它。4. 生成流程Prisma 主入口需要已生成的客户端执行pnpm run db:generateworkspace 根脚本/kysely与/enums子路径是纯类型/值在 type-check 阶段无需任何生成步骤本包不需要任何环境变量、不建立任何数据库连接——它只是类型与契约。真正的连接管理位于civitai/db包README 的 Gotchas 一节与package.json的依赖列表相互印证本包只依赖kysely与pg。Drift Detector对照真实数据库审计 schema 一致性该包最具特色的部分位于 src/schema-drift/其详细文档见 src/schema-drift/README.md。背景问题很直接schema 文件不等于数据库。本项目迁移按环境手工执行因此schema 文件里声明了、数据库里却没有的状态可能长期存在而无人察觉。这个工具把差距变成可数的数字。运行方式pnpm --filter civitai/db-schema drift # 文本报告 pnpm --filter civitai/db-schema drift --json # 机器可读 pnpm --filter civitai/db-schema drift --verbose # 附加被跳过的模型列表连接串来自环境变量DATABASE_URL——工具不内置任何关于数据库位置的信息也不应该内置。它是严格只读的每条语句都是对pg_catalog的SELECT从不写库、从不执行 DDL、从不应用迁移见 cli.ts 头部注释。完整 CLI 参数来自cli.ts的USAGEFlag作用--schema path指定要读取的 Prisma schema默认本包的prisma/schema.full.prisma--catalog path读取先前捕获的 catalog JSON 而非连接数据库--dump-catalog将 catalog 以 JSON 输出后退出与--catalog配对使用--db-schema name指定要内省introspect的 Postgres schema默认public--json以 JSON 输出漂移报告--verbose在文本报告中包含被跳过的模型列表--strict发现任何漂移时退出码为 1默认总是退出 0--strict刻意设计为可选项当前数据库真实存在一段漂移 backlog若门禁在任何发现时都失败会天天红、最终人人点过permanently-red gate 只会教会大家点掉它。在--strict下无法比较的 referential action 同样会使运行失败——未测量不等于干净。退出码 2 不可绕过一次什么都没比较的运行会打印出与健康数据库完全相同的干净页面——这正是--db-schema拼错、DATABASE_URL错误或角色无 catalog 可见性时的表现。CLI 会自行检查覆盖率并以 2 退出而不是报告一个安慰性的零。检查什么五类漂移检查项Schema 侧数据库侧外键存在每个拥有侧的relation(fields: […], references: […])pg_constraint中contype f且有序列元组相同引用动作onDelete/onUpdate显式或默认confdeltype/confupdtype列存在每个标量字段表上的pg_attribute行可空性字段可选性?pg_attribute.attnotnull唯一性unique、uniquepg_index中indisunique且无indpredmap/map全程解析为真实的表名与列名。判断细节中的几个坑源码注释的精华存在但动作错误是独立缺陷类约束存在看似不缺但数据库强制规则与 schema 承诺不一致ON DELETE与ON UPDATE都会被比较Prisma 默认onDelete不是Cascade可选关系默认SetNull必选关系默认Restrict——在必选关系上真相恰恰相反父级删除会被拒绝而非传播references:是读出来的从不假设为id多数关系引用id但也有引用projectId,position、serialId、userId、type、blockInstanceId的两种relation写法都被接受位置式relation(X, fields: …)与命名参数式relation(name: X, fields: …)。此前只处理前者时六个反向写法的关系既不出现在计数器里也不产生 finding——工具对外键报告干净实际上从未看过它们声明的列在表中不存在是独立 finding而非可空性不匹配Prisma 读取时会直接报 column does not exist块属性在去除注释后读取map/ignore/unique与模型体匹配避免回滚遗留的// ignore静默跳过整个模型映射到视图或不存在的表的模型会被跳过与ignore模型一起被计数和列出不视为漂移——没有表可供约束存在部分唯一索引不算数WHERE子句索引只对其命中的行强制唯一unique是对每一行的承诺表达式索引被丢弃而非按名匹配lower(email)索引不能伪装成email上的索引列聚合以text[]而非name[]选择node-postgres 未注册name[]的数组解析器array_agg(a.attname)会以字面量字符串{projectId,position}到达 JS——assertParsedArray会拒绝未解析的列表统一答复的 catalog 读取会被大声拒绝NOTNULL是 Postgres 保留字SELECT a.attnotnull notnull会解析为后缀IS NOT NULL运算符每行都返回常量true把每个可空列都读成NOT NULL——曾因此捏造出 626 个单向 finding。assertCatalogSanity现在直接让这类运行失败。不检查什么缺省不等于干净check 约束列默认值列类型含长度、精度与db.*原生类型枚举值与枚举成员非唯一索引、索引方法与排序主键id/id——唯一性检查只覆盖unique/unique本 schema 有 105 个id声明无一被验证程序化对象视图、函数、触发器、规则——例如BountyRank_Live视图实际发射了其提交定义没有的五个commentCount列数据库中多出但 schema 缺失的列与表反向方向才被检查行级安全、分区边界外键引用表是否与 schema 命名一致只匹配被约束的列元组内部结构纯函数驱动的四层流水线文件职责parse-prisma-schema.tsSchema → 模型、字段、映射、关系、唯一声明catalog.tspg_catalog→DbCatalog唯一与数据库对话的文件compare.ts(schema, catalog) → findings纯函数无 I/O、无时钟report.tsFindings → 文本报告differ 保持纯净pure因此可以从 fixtures 驱动测试包括刻意损坏的 fixture。cli.ts只负责参数解析与装配。drift:gatePR 级别的漂移增量门禁除了交互式报告还有 CI 半自动门禁见 gate-cli.tspnpm --filter civitai/db-schema drift:gate # 判定发现新的 enforced 漂移时退出 1 pnpm --filter civitai/db-schema drift:baseline # 接受当前 findings设计核心永不打开数据库连接gate-cli.ts没有任何数据库代码路径——它只把 schema 与已提交的 catalog 快照比较不存在任何能让它持凭据、解析主机名、把凭据打印进公开日志的 flag。这是硬性要求而非便利选择该仓库是公开的CI 日志也是公开的pg客户端在这里甚至没有被 import。两级严重性为什么这个切分是结构性的本项目迁移按环境手工应用schema 声明领先数据库是正常中间态而非缺陷。区分标准不是品味排序而是该 finding 涉及的数据库表面是否已存在级别判定门禁行为enforced列已在 catalog 中、约束却缺失使检查失败pending列不在 catalog 中schema 领先于它仅警告missing-column按构造恒为 pending列缺失本身就是 findingnullability与uniqueness按构造恒为 enforceddiffer 只对找到的列发出missing-foreign-key是唯一可二选一的种类——只要其任一被约束列不在 catalog 中即为 pending。级别可上升且上升会使检查失败被接受为 pending该列尚不存在的 finding在迁移落地却未带上约束的那一刻就变得可强制。指纹不变因此门禁比较当前级别与 baseline 记录的级别报告pending - enforced为失败——否则升级会被计入 matched 数量并以 0 退出。drift:baseline会打印每次吸收的升级使其进入 recapture 提交的日志而非无声消失。为什么用 baseline 而不是--strict--strict在任何 finding 时失败而main上现有 63 个 finding。天天红的门禁会在一周内被关掉。drift-baseline.json位于 src/schema-drift/drift-baseline.json将这 63 个记录为已接受门禁只报告不在其中的内容。baseline 已提交因此接受新漂移成为可评审的行为门禁会给出确切命令产生的 diff 让评审者精确看到这次改动放弃了哪个约束。指纹刻意排除declared/actual散文但把两项故意折叠进来——因为它们是 finding 本身而非关于它的散文可空性方向把字段翻转为必填却对着 NULLABLE 列不能继承旧条目的通过缺失外键的引用表把关系从Image改指向Post——同模型、同字段、同被约束列——同样不能继承。baseline 还被校验为与其捕获时的 catalog 匹配针对不同快照测量的 baseline 描述的是不同的数据库门禁会以退出码 2 拒绝比较。门禁能抓什么、不能抓什么数据库侧是冻结快照因此门禁只看到一个方向说明能抓schema 编辑承诺了被捕获数据库没有的东西抓不到数据库侧的任何动作——生产环境删除的约束对它不可见会退化捕获之后新建的列上的漂移只能永远 warn-only第三行最值得警惕这不是一次 recapture 就能修复的 bug而是与冻结工件比较的固有行为——快照越旧阻塞性覆盖越少而检查仍然绿色。因此每次运行 verdict 都会打印快照捕获日期与年龄超过 90 天会大声警告。这个工具诚实的名字是schema-edit gateschema 编辑门禁定期 recapture 快照才能让它更接近真正的 drift gate。另外要明确main有分支保护但没有required_status_checks红色门禁今天并不会阻止合并——它是响亮、可评审的信号而非联锁。测量数据对照提交快照2026-08-05__tests__/fixtures/catalog-production-2026-08-03.json是生产环境约束 catalog 的点位捕获仅 schema 元数据表/列名、可空性、外键与唯一索引列元组无行数据、无运行位置信息。用--catalog复现pnpm --filter civitai/db-schema drift \ --catalog src/schema-drift/__tests__/fixtures/catalog-production-2026-08-03.json当时的输出概览declared owning-side relations : 509 checked against the database : 448 skipped (view / absent table) : 61 MISSING foreign key : 40 wrong referential action : 0 - 未测量不是 干净 MISSING column : 18 nullability checked : 2348 nullability drift : 13 uniqueness declarations checked: 122 missing unique index : 1值得注意的几点referential action 的0是未测量——该快照早于该检查项没有ON DELETE/ON UPDATE数据408 个可比较外键全部报告为not comparable。真实运行会测量它们首次运行发现45 个全部是ON UPDATE声明Cascade、数据库NoActionON DELETE零不匹配由于id从不更新它们实践中是惰性的。可空性漂移从工具上线时的 246 降到 13#3592把七个*Rank家族列标记为可选以匹配数据库一次消灭了 235 个——这是真实的修复行动也解释了为何当时无人察觉CI 并未运行该包的测试套件直到 packages 被接入 CI。测试与验证体系pnpm --filter civitai/db-schema test测试策略的核心思想见 src/schema-drift/tests/一个什么都不返回的检测器与一个没接线的检测器无法区分直到你看着它完成两种行为阴性对照对齐的 (schema, catalog) 对被比较断言运行静默随后从 catalog 移除一个外键断言同一比较精确报告该键对零的正向对照对齐运行还断言非零的已检查计数4 个关系、3 个唯一声明、15 列使空的 finding 列表不可能来自什么都没比较的运行CLI 层的同一对cli.test.ts以进程方式运行真实入口空 catalog 必须退出 2 并提示 not trustworthy覆盖充分的 catalog 必须退出 0——没有第二个断言第一个可能因任何原因通过。catalog.test.ts用假查询驱动器驱动读取器覆盖行解码动作码、有序元组、未解析数组、表达式索引并钉死关键的 SQL 谓词indpred IS NULL、relkind IN (r,p)、WITH ORDINALITY、attname::text。文档明确提醒它不执行 SQL绿色套件不等于查询在真实服务器上行为正确——请通过实际运行工具来验证。CI 侧根目录vitest.config.mts的Package unit testsjob 通过pnpm run test:packages:run运行所有包套件并用台账脚本断言每个有 vitest 配置和测试文件的工作区包都出现在结果中且至少执行了一个非跳过测试——因为--project匹配空也会退出 0glob 停止解析也会退出 0自我跳过的套件同样退出 0。该 job 并非联锁无required_status_checks但它让红色可见而非被忽略。接入与使用要点速查依赖civitai/db-schema: workspace:*通常随civitai/db作为 peer 带入仅在自行导入DB类型或枚举时显式添加转译Next 配transpilePackages、Vite 配ssr.noExternal与civitai/db一同处理生成Prisma 入口需要pnpm run db:generate/kysely、/enums无需生成即可通过类型检查环境导出面不需要任何 env、不连库仅 drift 检测器读取DATABASE_URL漂移审计drift文本/JSON/verbose用于人工巡检--strict用于有干净基线可守的调用方--catalog/--dump-catalog支持离线快照比对CI 门禁drift:gate只拦截在数据库已存在的列上新增 enforced 漂移与pending - enforced升级drift:baseline将接受行为变成可评审的提交差异快照保鲜recapture 快照drift --dump-catalog与刷新 baseline 应在同一提交内完成并留意新纳入范围的 45 个 referential-action finding 需逐条分类处理。注意事项本包是纯契约层——没有 env、没有 DB 连接、只有类型与值。所有连接与读写都发生在civitai/db及其上层服务中任何把本包当作运行时数据库入口使用的做法都偏离了它的设计边界。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表