跳到内容
CADCAMFUN
文档

文档

数据库

SQLite 持久性具有范围连接所有权、类型化查询失败、迁移备份和本地文档存储。

从英文自动翻译。 查看英文原文

持久化模型

@cadcamfun/db 通过以下方式打开 SQLite bun:sqlite 并在 Effect 中公开了一个 Drizzle 数据库 Database 服务。 Sidecar 将其应用程序数据库存储在 <dataDir>/cadcamfun.db。该连接在应用迁移之前启用预写日志记录和外键强制执行。

该数据库包含项目及其相关资源的工作副本。项目版本控制对象、拉取请求和工作流程运行也存储在本地。 AI 提供商凭证使用单独的所有者可读的 JSON 文件;应用程序承载令牌在启动时提供,而不是作为首选项保留。

架构和入口点

packages/db/src/schema.ts 定义表、索引、关系、默认值和推断的选择/插入类型。 src/index.ts 出口 Database, DatabaseError、细雨 Db 类型和模式命名空间。 src/upgrade.ts 拥有迁移规划、备份和事务应用程序; src/migrate.ts 是命令行入口点。生成的 SQL 和迁移日志位于 packages/db/drizzle.

每个打开的连接 packages/db/src/index.ts 将 SQLite 切换到 journal_mode = WAL 并打开 foreign_keys。关闭数据库运行 wal_checkpoint(TRUNCATE) 所以当进程退出时主文件就完成了。如果检查点繁忙或失败,则会记录警告并继续关闭。

存储值

域标识符默认为由表配置的 UUID。 Drizzle 将时间戳列映射到 JavaScript 日期,将 JSON 列映射到声明的记录类型。 HTTP 存储库将日期转换为其有线表示形式并在域边界验证 JSON。存储的 JSON 列上的 TypeScript 类型注释本身并不能证明导入的或较旧的数据与当前域架构匹配。

项目拥有自己的工程图、组件和刀具路径。刀具路径可以参考图纸、材料、刀具和机器配置;他们的 data 现场可携带便携式制造文件。除了整个项目的 Git 历史记录之外,单独的绘图、组件和刀具路径版本表还保留资源版本。

通过服务查询

Database.layer 接受文件路径或 :memory:、一个可选的迁移文件夹和一个可选的备份目录。它获取其 Effect 范围的 SQLite 句柄。呼叫者使用 query(operation, run) 执行同步或异步 Drizzle 工作;外国例外成为 DatabaseError 包含操作名称和原始原因。

存储库查询
import { Database, schema } from "@cadcamfun/db"
import { Effect } from "effect"

const loadProjects = Effect.gen(function* () {
  const database = yield* Database
  return yield* database.query("projects.list", (db) =>
    db.select().from(schema.projectTable).all())
})

存储库拥有自己的访问规则和行解释;共享查询助手提供失败归因。它不会自动授权查询或围绕任意序列的服务调用创建事务。

升级行为

打开带有迁移文件夹的图层会读取迁移文件并将其名称与 __drizzle_migrations。旧格式的日志通过匹配迁移时间戳和哈希值进行回填。不匹配的日志会使升级失败,而不是猜测迁移身份。

当现有磁盘数据库需要升级时,该服务会使用以下命令创建迁移前 SQLite 备份 VACUUM INTO。它写入部分文件,重命名已完成的快照,并保留最新的五个匹配的迁移前备份。默认备份目录是 backups 在数据库旁边。新数据库和内存数据库不需要此升级前备份。

日志更改和挂起的 SQL 语句在一个即时事务中运行。失败的迁移会回滚事务并导致层构建失败。当数据库初始化失败时,应用程序不应宣布准备就绪。

迁移命令

从存储库根运行
bun run db:generate
bun run db:migrate

更改后生成迁移 schema.ts,并将生成的 SQL 和日志保存在一起。迁移 CLI 使用 DATABASE_URL 提供时,删除初始 file: 前缀,否则打开 ./data/cadcamfun.db。根包装器运行此脚本 packages/db 作为它的工作目录,所以它的默认目标是 packages/db/data/cadcamfun.db。网络工作室使用 data/cadcamfun.db 在存储库根目录下。打包的 sidecar 使用 CADCAMFUN_DATA_DIR。运行维护时选择显式数据库路径,以便这些入口点不会无意中对不同的文件进行操作。

没有迁移文件夹, Database.layer 打开数据库但不创建应用程序表。需要完整架构的测试为迁移目录提供内存数据库。

数据域

  • 身份与协作: 用户、帐户、会话、身份验证器、验证令牌、订阅、组织、成员资格、邀请、对话、消息、通知和推送订阅。
  • 设计与制造: 项目、图纸、组件、其版本和注释、材料、工具、机器配置、刀具路径和生产成本记录。
  • 申请服务: 文件、库项目、插件注册表/配置、API 密钥、分析和活动记录、市场数据、MCP 配置和本地键值条目。
  • 项目历史: Git 存储库、对象和引用、拉取请求和评论以及工作流程运行。

表的存在建立了持久性模型。网络集成和应用程序功能取决于它们的实际处理程序和服务,而不仅仅是表的存在。

本地键值存储

LocalKeyValue 保留 JSON 兼容的应用程序状态。边车 KvStore 支持获取、更新插入、删除和前缀过滤列表。键必须包含非空白文本且最多为 512 个字符。值允许 null、字符串、布尔值、有限数字、数组和普通对象;循环、函数、非有限数和非普通对象将被拒绝。

条目包括更新时间戳。更新插入替换相同键的值。前缀列表按键排序并在内存中进行过滤。使用此存储来获取偏好和辅助上下文;凭证属于他们的专用凭证服务。

关机和恢复

数据库范围终结器请求 PRAGMA wal_checkpoint(TRUNCATE) 在关闭手柄之前。繁忙的检查点或关闭失败会被记录为警告,以便剩余的终结器可以运行。连接所有者(而不是各个 HTTP 处理程序)关闭句柄。

为了获得一致的手动备份,请在复制其数据目录之前彻底停止应用程序,或者在其运行时使用 SQLite 的备份工具。实时数据库可以在其 WAL 文件中拥有状态。要检查迁移前备份,请处理副本并保留原始数据库和备份,直到确认恢复为止。

存储库职责

将 SQLite 连接创建和迁移逻辑保留在此包中。 Sidecar 存储库使用提供的数据库服务并将存储故障映射到其资源的 HTTP 错误。将几何计算保留在核心中,并将秘密保留在凭证或令牌服务中。数据库备份包含项目和帐户数据,并且需要与活动数据目录相同的访问控制。

检查数据库而不更改它

干净关闭后,针对显式数据库路径使用 SQLite CLI。只读模式可避免在路径错误时意外创建新的空文件。这些检查检查结构和一致性,而不会暴露帐户或凭证行。

只读 SQLite 检查
sqlite3 -readonly /absolute/path/cadcamfun.db 'PRAGMA quick_check;'
sqlite3 -readonly /absolute/path/cadcamfun.db 'PRAGMA foreign_key_check;'
sqlite3 -readonly /absolute/path/cadcamfun.db "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;"
sqlite3 -readonly /absolute/path/cadcamfun.db 'SELECT name, applied_at FROM __drizzle_migrations ORDER BY id;'

quick_check 通常报告 ok; foreign_key_check 仅返回违规行。一本没有的日记 name/applied_at columns 是一种较旧的格式,应用程序升级代码必须在使用最后一个查询之前协调该格式。在尝试修复之前检查路径和迁移身份。

应用程序在其自己的连接上设置外键强制执行。单独打开的维护连接必须在进行授权写入之前显式启用它; PRAGMA 是特定于连接的。

来源: packages/db/src/index.ts, upgrade.ts, 和 database.test.ts.

在代码中使用一次性数据库

完整的内存中服务范围对于存储库示例和隔离诊断非常有用。范围拥有连接并在效果读取服务之前应用签入的迁移。

开放、迁移、查询、发布
import { Database, schema } from "@cadcamfun/db"
import { Effect } from "effect"

const migrationsFolder = new URL("../../packages/db/drizzle", import.meta.url).pathname
const program = Effect.gen(function* () {
  const database = yield* Database
  return yield* database.query("diagnostic.projects", (db) =>
    db.select({ id: schema.projectTable.id }).from(schema.projectTable).all())
}).pipe(Effect.provide(Database.layer({ path: ":memory:", migrationsFolder })))
console.log(await Effect.runPromise(program)) // []

将此脚本另存为 apps/sidecar/diagnostic.ts 并运行 bun apps/sidecar/diagnostic.ts 从存储库根目录。该包提供数据库和 Effect 依赖项,并且迁移 URL 从该文件解析。丢失或错误的迁移目录并不等同于空的应用程序架构。细雨 query 回调可以运行同步查询或返回承诺;帮助器将失败包装在提供的操作标签下。

来源: packages/db/src/database.test.ts 和 index.ts.

诊断存储故障

  • 没有这样的表: 确认确切的数据库文件和迁移文件夹。服务允许在不迁移的情况下打开,但查询应用程序表会失败。
  • 外键约束失败: 检查引用的所有者/项目/资源。删除限制是特定于架构的:删除具有所属项目的用户受到限制,而删除用户时会话行会级联。
  • 升级期间备份失败: 检查数据库和备份目录中的可用空间和写入权限。备份失败停止迁移;不要删除原始数据库来绕过它。
  • 旧迁移日志与本地迁移不匹配: 保留数据库并恢复匹配的迁移集。重命名或删除日志行将隐藏不兼容的升级历史记录。
  • 检查点忙于关闭: 在仅复制数据库文件之前检查其他打开的连接。保留 WAL/SHM 同级,直到干净的检查点或一致的 SQLite 备份完成。

迁移备份是具有五个文件保留策略的升级前恢复点,而不是后续编辑的连续备份。根据安装的恢复要求,单独备份完整的数据目录。

来源: packages/db/src/index.ts, upgrade.ts, schema.ts, 和 database.test.ts.

迁徙历史

签入的迁移按文件夹顺序应用。每个文件夹包含一个 migration.sql 和毛毛雨套件 snapshot.json 用于计算下一个差异。日志行存储文件夹名称、哈希值、文件夹时间戳(以毫秒为单位)(created_at),以及应用的 ISO 时间(applied_at).

文件夹尺寸它创造了什么
20261004223029_careful_monster_badoon771行The baseline schema converted from the legacy Prisma model: identity, organizations, projects, drawings, components, toolpaths, libraries, costs, marketplace, plugins and MCP configuration.表和列名称未更改,因此较旧 dev.db 数据保持可读。
20261008030518_redundant_dagger73行项目历史: GitRepo, GitObject, GitRef, PullRequest, PullRequestReview, WorkflowRun.
20261009012351_moaning_loki28行几何内核记录: MeshAsset 和 CadOperationLog.
20261009171602_brown_mad_thinker5行LocalKeyValue (key 文本主键, value 文本不为空, updatedAt 整数不为空)。

文件夹名称来自 drizzle-kit:UTC 时间戳后跟随机名称。一旦文件夹被应用到任何地方,就不要重命名或编辑它;升级规划器通过名称来识别应用的迁移,对于较旧的日志,通过时间戳和哈希来识别。

升级决策流程

打开文件WAL + 外键 planUpgrade阅读日志+文件 需要吗?遗留或待处理 > 0 否:毛毛雨()服务就绪 backupDatabase仅当文件存在时 applyUpgrade一笔即时交易 失败数据库错误,回滚 是的
情况备份结果
新文件,给出的迁移文件夹否(打开前文件不存在)所有迁移都在一笔事务中应用
现有文件、日志是最新的不按原样打开; needed 是假的
现有文件,待迁移是的,在任何改变之前已应用待定迁移;日记帐行插入 applied_at
现有文件、日志无 name 柱子是的专栏 name 和 applied_at 添加,回填行,然后应用挂起的迁移
旧行与本地迁移不匹配不采取备份:计划首先失败DatabaseError 与操作 migrate:“旧迁移日志与本地迁移不匹配”
:memory:绝不如果给定文件夹则应用迁移
没有迁移文件夹绝不不创建表打开

回填匹配的工作原理如下:遗留行的 created_at 被截断为整秒并与每个迁移的文件夹时间戳进行比较。如果只有一个迁移匹配,则使用它。否则,哈希会打破平局,并且作为最后的手段,使用具有相同哈希的任何迁移。

备份文件剖析

迁移前备份名称
backups/cadcamfun-20261015-034512.123-6f1c2d4e-8a90-4b7c-9e21-3f5a6b7c8d9e-pre-migrate.db
        └ prefix ┘ └ UTC timestamp ┘ └ random UUID                      ┘
  • 前缀是数据库文件名,不带前缀 .db,后跟破折号。
  • 时间戳为 UTC,精度为毫秒。如果最新的现有备份具有相同或较晚的时间戳,则新备份会在其之后设置 1 毫秒,因此词汇顺序始终等于创建顺序。
  • 快照被写入 <name>.partial 和 VACUUM INTO,然后重命名。一次崩溃最多留下一个 .partial 文件,该文件在 finally 堵塞。
  • 成功备份后,仅保留与确切模式匹配的五个最新文件。您以其他名称复制到目录中的文件永远不会被删除。
  • VACUUM INTO 生成一个紧凑的、独立的数据库,没有 WAL 兄弟,因此可以直接打开备份。
恢复迁移前备份(应用程序已停止)
DATA="/absolute/path/to/data"
cp "$DATA/cadcamfun.db" "$DATA/cadcamfun.db.broken"         # keep the failed state
rm -f "$DATA/cadcamfun.db-wal" "$DATA/cadcamfun.db-shm"
cp "$DATA/backups/cadcamfun-<timestamp>-<uuid>-pre-migrate.db" "$DATA/cadcamfun.db"
sqlite3 -readonly "$DATA/cadcamfun.db" 'PRAGMA quick_check;'   # expect: ok

恢复回滚架构和数据

恢复的备份是升级前的状态。升级后所做的编辑不包含在其中,下次启动时将再次尝试相同的迁移。重新启动之前修复故障原因。

核心台形状

这些是桌面编辑器、历史记录和内核最常阅读的表格。列名是 SQL 名称;毛毛雨将它们暴露在相同的键下。

桌子专栏
Projectid, name, description, createdAt, updatedAt, ownerId, organizationId, isPublic
Drawingid, name, description, data, thumbnail, createdAt, updatedAt, projectId
Componentid, name, description, data, thumbnail, type, isPublic, createdAt, updatedAt, projectId
Toolpathid, name, description, data, type, operationType, gcode, thumbnail, isPublic, projectId, createdAt, updatedAt, createdBy, drawingId, materialId, toolId, machineConfigId
GitRepoprojectId, head, remote, createdAt
GitObjectprojectId, oid, type, body
GitRefprojectId, name, oid
PullRequestid, projectId, number, title, body, head, base, state, authorId, authorKind, mergeCommit, remoteNumber, createdAt, updatedAt
WorkflowRunid, projectId, workflow, event, ref, sha, status, events, startedAt, finishedAt
MeshAssetid, projectId, kind, inputs, stats, mesh, createdAt
CadOperationLogid, projectId, operation, meshAssetId, outcome, durationMs, inputs, error, traceId, spanId, createdAt
LocalKeyValuekey, value, updatedAt
项目 绘画成分刀具路径Git* 表PullRequestMeshAssetWorkflowRun DrawingVersion比较。版本刀具路径版本。CadOpLogCadOpLog.mesh→空 实线:ON DELETE CASCADE

删除项目会删除其图纸、组件、刀具路径、历史对象和参考、拉取请求、工作流程运行、网格资产和操作日志。版本和注释行从其父资源级联。一个 CadOperationLog 行在删除其网格资产后仍然存在 meshAssetId 设置为空。到材料、刀具和机器配置的刀具路径链接没有级联,因此当刀具路径指向参考材料时,无法删除它。

全表目录

该架构定义了 60 表。下面的列表是根据 Drizzle 表定义生成的:SQL 名称、导出常量、列数、声明的索引数以及外键及其删除规则。 “无操作”是 SQLite 的默认设置:当引用行存在时删除失败(在应用程序连接上强制使用外键)。

SQL表出口科尔斯idx外键
AccountaccountTable131userId → 用户(级联)
ActivityLogactivityLogTable70userId → 用户(无操作)
AIAnalysisLogaIAnalysisLogTable70userId → 用户(无操作)
ApiKeyapiKeyTable71userId → 用户(级联)
authenticatorsauthenticatorTable80userId → 用户(级联)
CadOperationLogcadOperationLogTable111projectId → 项目(级联)
meshAssetId → MeshAsset(设置为空)
ComponentcomponentTable100projectId → 项目(级联)
ComponentCommentcomponentCommentTable60componentId → 组件(级联)
userId → 用户(无操作)
ComponentVersioncomponentVersionTable60componentId → 组件(级联)
userId → 用户(无操作)
ConversationconversationTable60organizationId → 组织(级联)
ConversationParticipantconversationParticipantTable51userId → 用户(级联)
conversationId → 对话(级联)
CostSettingscostSettingsTable110ownerId → 用户(无操作)
organizationId → 组织(无行动)
DrawingdrawingTable80projectId → 项目(级联)
DrawingVersiondrawingVersionTable50drawingId → 绘图(级联)
FileUploadfileUploadTable120—
GitObjectgitObjectTable41projectId → 项目(级联)
GitRefgitRefTable31projectId → 项目(级联)
GitRepogitRepoTable40projectId → 项目(级联)
LibraryItemlibraryItemTable140ownerId → 用户(无操作)
organizationId → 组织(无行动)
LocalKeyValuelocalKeyValueTable30—
MachineConfigmachineConfigTable90ownerId → 用户(无操作)
MarketplaceCategorymarketplaceCategoryTable101parentId → 市场类别(无操作)
MarketplaceCollectionmarketplaceCollectionTable101sellerId → 市场卖家(无操作)
MarketplaceCollectionItemmarketplaceCollectionItemTable51collectionId → MarketplaceCollection(级联)
itemId → 市场项目(级联)
MarketplaceItemmarketplaceItemTable305sellerId → 市场卖家(无操作)
MarketplaceOrdermarketplaceOrderTable234buyerId → 用户(无操作)
sellerId → 市场卖家(无操作)
itemId → MarketplaceItem(无操作)
MarketplacePayoutmarketplacePayoutTable121sellerId → 市场卖家(无操作)
MarketplaceReviewmarketplaceReviewTable122itemId → MarketplaceItem(无操作)
buyerId → 用户(无操作)
orderId → 市场订单(无操作)
MarketplaceSellermarketplaceSellerTable190userId → 用户(无操作)
MarketplaceWishlistmarketplaceWishlistTable41userId → 用户(级联)
itemId → 市场项目(级联)
MaterialmaterialTable90ownerId → 用户(无操作)
organizationId → 组织(无行动)
MaterialCostmaterialCostTable100materialId → 材料(级联)
ownerId → 用户(无操作)
organizationId → 组织(无行动)
MCPServerConfigmCPServerConfigTable111—
MeshAssetmeshAssetTable71projectId → 项目(级联)
MessagemessageTable81senderId → 用户(级联)
conversationId → 对话(级联)
fileId → 文件上传(设置为空)
NotificationnotificationTable120userId → 用户(级联)
organizationId → 组织(级联)
projectId → 项目(级联)
componentId → 组件(级联)
messageId → 消息(级联)
OperationCostoperationCostTable100ownerId → 用户(无操作)
organizationId → 组织(无行动)
OrganizationorganizationTable50—
OrganizationInvitationorganizationInvitationTable70organizationId → 组织(级联)
passkeypasskeyTable110userId → 用户(级联)
PluginConfigurationpluginConfigurationTable20pluginId → PluginRegistryEntry(级联)
PluginRegistryEntrypluginRegistryEntryTable90—
ProductionCostEstimateproductionCostEstimateTable170toolpathId → 刀具路径(级联)
operationCostId → 运营成本(不采取行动)
ownerId → 用户(无操作)
organizationId → 组织(无行动)
ProjectprojectTable80ownerId → 用户(无操作)
organizationId → 组织(无行动)
PullRequestpullRequestTable141projectId → 项目(级联)
authorId → 用户(无操作)
PullRequestReviewpullRequestReviewTable81pullRequestId → PullRequest(级联)
reviewerId → 用户(无操作)
PushSubscriptionpushSubscriptionTable60userId → 用户(级联)
SessionsessionTable80userId → 用户(级联)
SubscriptionsubscriptionTable243referenceId → 用户(级联)
TokentokenTable82userId → 用户(级联)
TooltoolTable170ownerId → 用户(无操作)
organizationId → 组织(无行动)
ToolpathtoolpathTable170projectId → 项目(级联)
createdBy → 用户(无操作)
drawingId → 绘图(无动作)
materialId → 材料(无动作)
toolId → 工具(无动作)
machineConfigId → MachineConfig(无操作)
ToolpathCommenttoolpathCommentTable60toolpathId → 刀具路径(级联)
userId → 用户(无操作)
ToolpathVersiontoolpathVersionTable70toolpathId → 刀具路径(级联)
userId → 用户(无操作)
ToolWearCosttoolWearCostTable100toolId → 工具(级联)
ownerId → 用户(无操作)
organizationId → 组织(无行动)
twoFactortwoFactorTable40userId → 用户(级联)
UseruserTable170—
UserOrganizationuserOrganizationTable51userId → 用户(级联)
organizationId → 组织(级联)
verification_tokensverificationTokenTable31—
WorkflowRunworkflowRunTable101projectId → 项目(级联)

安全书写

  • 将 Drizzle 事务中的多语句更改包装在一个事务中 query 称呼: database.query("op", (db) => db.transaction((tx) => ...))。分离 query 调用是单独的事务。
  • 为每个查询提供一个稳定的、点分的操作名称(projects.list, toolpaths.update)。这是唯一的上下文 DatabaseError 除了原因之外,它还出现在日志中。
  • 生成 id newId() (UUID)或让列默认执行此操作;不要根据名称构建 id。
  • 读取 JSON 列时验证它们。 SQLite 不会检查声明的 TypeScript 类型。
  • 使用 LocalKeyValue 通过 sidecar 键值服务而不是直接写入行,因此应用键和值验证。
事务性写入
import { Database, schema } from "@cadcamfun/db"
import { eq } from "drizzle-orm"
import { Effect } from "effect"

const renameProject = (id: string, name: string) => Effect.gen(function* () {
  const database = yield* Database
  return yield* database.query("projects.rename", (db) =>
    db.transaction((tx) => {
      const updated = tx.update(schema.projectTable)
        .set({ name, updatedAt: new Date() })
        .where(eq(schema.projectTable.id, id))
        .returning().all()
      if (updated.length === 0) throw new Error(`project ${id} not found`)
      return updated[0]
    }))
})

要创建具有显式 id 的行,请使用 schema.newId(); @cadcamfun/db 将架构模块重新导出为 schema 命名空间,而不是顶级名称。

在 3D

3D 中的数据库架构 拖动进行轨道旋转·滚动进行缩放·右键拖动进行平移
来自 Drizzle 迁移的所有 60 表位于packages/db/drizzle 中;每行都是一个 REFERENCES 外键(不同表之间的 91)。中心的中心是引用次数最多的表。