关于 data-model-changes
为 Inkeep Agent Framework 贡献者提供数据模型变更指南,涵盖模式、验证、类型、数据访问和迁移,并区分版本化配置数据与运行时事务数据。还包括 Dolt 分支迁移检查。
主要功能
- 01Manage 与 runtime 数据库说明
- 02模式、关系和验证模式
- 03实体类型与数据访问步骤
- 04迁移生成及分支检查
- 05Changeset 与迁移安全规则
适用场景
- 01新增数据表或列
- 02扩展表关系和数据访问
- 03应用数据库模式迁移
- 04验证跨分支的 manage 迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ inkeep/ agents --skill data-model-changes安装后在 Claude Code 中输入/data-model-changes确认已加载。
详细介绍
数据模型变更指南
在 Inkeep Agent Framework 中变更数据模型(数据库架构、验证、类型和数据访问层)的全面指南。
数据库架构
该框架使用两个独立的 PostgreSQL 数据库:
| 数据库 | 配置文件 | 架构文件 | 用途 |
|---|---|---|---|
| Manage(Doltgres) | drizzle.manage.config.ts | src/db/manage/manage-schema.ts | 版本化配置:项目、Agent、工具、触发器、评估器 |
| Runtime(Postgres) | drizzle.run.config.ts | src/db/runtime/runtime-schema.ts | 事务数据:对话、消息、任务、API 密钥 |
主要区别:
- Manage DB:很少变更的配置(Agent 定义、工具配置)。支持 Dolt 版本控制。
- Runtime DB:高频事务数据(对话、消息)。与 manage 表之间没有跨数据库外键。
架构模式
1. 作用域模式(多租户)
所有表都使用分层作用域。使用以下可复用字段模式:
// Tenant-level (org-wide resources)
const tenantScoped = {
tenantId: varchar('tenant_id', { length: 256 }).notNull(),
id: varchar('id', { length: 256 }).notNull(),
};
// Project-level (project-specific resources)
const projectScoped = {
...tenantScoped,
projectId: varchar('project_id', { length: 256 }).notNull(),
};
// Agent-level (agent-specific resources)
const agentScoped = {
...projectScoped,
agentId: varchar('agent_id', { length: 256 }).notNull(),
};
// Sub-agent level (sub-agent-specific resources)
const subAgentScoped = {
...agentScoped,
subAgentId: varchar('sub_agent_id', { length: 256 }).notNull(),
};
在实际表中的用法示例:
// Project-scoped: tools belong to a project
export const tools = pgTable(
'tools',
{
...projectScoped, // tenantId, id, projectId
name: varchar('name', { length: 256 }).notNull(),
config: jsonb('config').$type<ToolConfig>().notNull(),
...timestamps,
},
(table) => [
primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
]
);
// Agent-scoped: triggers belong to an agent
export const triggers = pgTable(
'triggers',
{
...agentScoped, // tenantId, id, projectId, agentId
...uiProperties,
enabled: boolean('enabled').notNull().default(true),
...timestamps,
},
(table) => [
primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
]
);
// Sub-agent scoped: tool relations belong to a sub-agent
export const subAgentToolRelations = pgTable(
'sub_agent_tool_relations',
{
...subAgentScoped, // tenantId, id, projectId, agentId, subAgentId
toolId: varchar('tool_id', { length: 256 }).notNull(),
...timestamps,
},
(table) => [
primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
]
);
2. 常见字段模式
// Standard UI properties
const uiProperties = {
name: varchar('name', { length: 256 }).notNull(),
description: text('description'),
};
// Standard timestamps
const timestamps = {
createdAt: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
};
用法示例:
// Table with UI properties (user-facing entity)
export const projects = pgTable(
'projects',
{
...tenantScoped,
...uiProperties, // name (required), description (optional)
models: jsonb('models').$type<ProjectModels>(),
...timestamps, // createdAt, updatedAt
},
(table) => [primaryKey({ columns: [table.tenantId, table.id] })]
);
// Table without UI properties (internal/join table)
export const subAgentRelations = pgTable(
'sub_agent_relations',
{
...agentScoped,
sourceSubAgentId: varchar('source_sub_agent_id', { length: 256 }).notNull(),
targetSubAgentId: varchar('target_sub_agent_id', { length: 256 }),
relationType: varchar('relation_type', { length: 256 }),
...timestamps, // Still include timestamps for auditing
},
(table) => [
primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
]
);
3. JSONB 类型注解
始终使用 .$type<T>() 为 JSONB 列添加注解,以确保类型安全:
models: jsonb('models').$type<Models>(),
config: jsonb('config').$type<{ type: 'mcp'; mcp: ToolMcpConfig }>().notNull(),
metadata: jsonb('metadata').$type<ConversationMetadata>(),
添加新表
步骤 1:在架构中定义表
位置:packages/agents-core/src/db/manage/manage-schema.ts(配置)或 runtime-schema.ts(运行时)
export const myNewTable = pgTable(
'my_new_table',
{
...projectScoped, // Choose appropriate scope
...uiProperties, // If it has name/description
// Custom fields
status: varchar('status', { length: 50 }).notNull().default('active'),
config: jsonb('config').$type<MyConfigType>(),
// Reference fields (optional)
parentId: varchar('parent_id', { length: 256 }),
...timestamps,
},
(table) => [
// Primary key - ALWAYS include all scope fields
primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
// Foreign keys (only within same database!)
foreignKey({
columns: [table.tenantId, table.projectId],
foreignColumns: [projects.tenantId, projects.id],
name: 'my_new_table_project_fk',
}).onDelete('cascade'),
// Optional: indexes for frequent queries
index('my_new_table_status_idx').on(table.status),
]
);
步骤 2:添加关联(如有需要)
export const myNewTableRelations = relations(myNewTable, ({ one, many }) => ({
project: one(projects, {
fields: [myNewTable.tenantId, myNewTable.projectId],
references: [projects.tenantId, projects.id],
}),
// Add more relations as needed
}));
步骤 3:创建 Zod 验证架构
位置:packages/agents-core/src/validation/schemas.ts
// Create base schemas from Drizzle table
export const MyNewTableSelectSchema = registerFieldSchemas(
createSelectSchema(myNewTable)
).openapi('MyNewTable');
export const MyNewTableInsertSchema = registerFieldSchemas(
createInsertSchema(myNewTable)
).openapi('MyNewTableInsert');
export const MyNewTableUpdateSchema = MyNewTableInsertSchema.partial()
.omit({ tenantId: true, projectId: true, id: true, createdAt: true })
.openapi('MyNewTableUpdate');
// API schemas (omit internal scope fields)
export const MyNewTableApiSelectSchema = createApiSchema(MyNewTableSelectSchema)
.openapi('MyNewTableApiSelect');
export const MyNewTableApiInsertSchema = createApiInsertSchema(MyNewTableInsertSchema)
.openapi('MyNewTableApiInsert');
export const MyNewTableApiUpdateSchema = createApiUpdateSchema(MyNewTableUpdateSchema)
.openapi('MyNewTableApiUpdate');
步骤 4:创建实体类型
位置:packages/agents-core/src/types/entities.ts
export type MyNewTableSelect = z.infer<typeof MyNewTableSelectSchema>;
export type MyNewTableInsert = z.infer<typeof MyNewTableInsertSchema>;
export type MyNewTableUpdate = z.infer<typeof MyNewTableUpdateSchema>;
export type MyNewTableApiSelect = z.infer<typeof MyNewTableApiSelectSchema>;
export type MyNewTableApiInsert = z.infer<typeof MyNewTableApiInsertSchema>;
export type MyNewTableApiUpdate = z.infer<typeof MyNewTableApiUpdateSchema>;
步骤 5:创建数据访问函数
位置:packages/agents-core/src/data-access/manage/myNewTable.ts(或 runtime/)
import { and, eq, desc, count } from 'drizzle-orm';
import type { AgentsManageDatabaseClient } from '../../db/manage/manage-client';
import { myNewTable } from '../../db/manage/manage-schema';
import type { MyNewTableInsert, MyNewTableSelect, MyNewTableUpdate } from '../../types/entities';
import type { ProjectScopeConfig, PaginationConfig } from '../../types/utility';
export const getMyNewTableById =
(db: AgentsManageDatabaseClient) =>
async (params: {
scopes: ProjectScopeConfig;
itemId: string;
}): Promise<MyNewTableSelect | undefined> => {
const { scopes, itemId } = params;
return db.query.myNewTable.findFirst({
where: and(
eq(myNewTable.tenantId, scopes.tenantId),
eq(myNewTable.projectId, scopes.projectId),
eq(myNewTable.id, itemId)
),
});
};
export const listMyNewTable =
(db: AgentsManageDatabaseClient) =>
async (params: { scopes: ProjectScopeConfig }): Promise<MyNewTableSelect[]> => {
return db.query.myNewTable.findMany({
where: and(
eq(myNewTable.tenantId, params.scopes.tenantId),
eq(myNewTable.projectId, params.scopes.projectId)
),
});
};
export const createMyNewTable =
(db: AgentsManageDatabaseClient) =>
async (params: MyNewTableInsert): Promise<MyNewTableSelect> => {
const result = await db.insert(myNewTable).values(params as any).returning();
return result[0];
};
export const updateMyNewTable =
(db: AgentsManageDatabaseClient) =>
async (params: {
scopes: ProjectScopeConfig;
itemId: string;
data: MyNewTableUpdate;
}): Promise<MyNewTableSelect> => {
const result = await db
.update(myNewTable)
.set({ ...params.data, updatedAt: new Date().toISOString() } as any)
.where(
and(
eq(myNewTable.tenantId, params.scopes.tenantId),
eq(myNewTable.projectId, params.scopes.projectId),
eq(myNewTable.id, params.itemId)
)
)
.returning();
return result[0];
};
export const deleteMyNewTable =
(db: AgentsManageDatabaseClient) =>
async (params: { scopes: ProjectScopeConfig; itemId: string }): Promise<void> => {
await db.delete(myNewTable).where(
and(
eq(myNewTable.tenantId, params.scopes.tenantId),
eq(myNewTable.projectId, params.scopes.projectId),
eq(myNewTable.id, params.itemId)
)
);
};
步骤 6:从数据访问索引导出
位置:packages/agents-core/src/data-access/index.ts
export * from './manage/myNewTable';
步骤 7:生成并应用迁移
# Generate migration SQL
pnpm db:generate
# Review generated SQL in drizzle/manage/ or drizzle/runtime/
# Make minor edits if needed (ONLY to newly generated files)
# Apply migration
pnpm db:migrate
步骤 8:验证跨 Dolt 分支的 manage 迁移
如果变更涉及 manage 架构,不要在迁移成功应用于 main 后就停止。你还应验证架构变更能否从 main 架构分支合并到包含真实数据的现有项目分支。
Doltgres 仍处于测试阶段,且有效的 postgres 与 doltgres 行为之间可能存在差异。这些差异可能导致迁移在合并到项目分支时失败。
建议的验证流程:
# 1. Ensure your local Dolt DB already has representative test data
# in main and in one or more non-main branches
# 2. Apply the migration on main
pnpm db:manage:migrate
# 3. Push the schema change from main to all non-main branches
pnpm --filter @inkeep/agents-core db:manage:sync-all-branches
需要检查的内容:
SYNCED表示该分支的架构已顺利更新NOOP表示该分支已是最新状态- 任何失败都表示迁移可能存在分支合并问题,发布前必须修复
此检查可发现:
- Dolt 特有的约束、索引或表重写合并失败
向现有表添加列
步骤 1:修改架构
在表定义中添加新列:
// In manage-schema.ts or runtime-schema.ts
export const existingTable = pgTable(
'existing_table',
{
// ... existing fields ...
// New column
newField: varchar('new_field', { length: 256 }),
newJsonField: jsonb('new_json_field').$type<MyNewType>().default(null),
...timestamps,
},
// ... constraints ...
);
步骤 2:更新 Zod 架构(如需自定义验证)
如果该字段需要超出 Drizzle 默认行为的自定义验证:
// In schemas.ts, update field schemas if needed
registerFieldSchemas(existingSchema, {
newField: (schema) => schema.min(1).max(100),
});
步骤 3:生成并应用迁移
pnpm db:generate
# Review the generated migration SQL
pnpm db:migrate
添加表之间的关联
多对多关系的联接表
export const entityAEntityBRelations = pgTable(
'entity_a_entity_b_relations',
{
...projectScoped, // Appropriate scope
entityAId: varchar('entity_a_id', { length: 256 }).notNull(),
entityBId: varchar('entity_b_id', { length: 256 }).notNull(),
// Optional: relation-specific fields
config: jsonb('config').$type<RelationConfig>(),
...timestamps,
},
(table) => [
primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
// Foreign keys to both tables
foreignKey({
columns: [table.tenantId, table.projectId, table.entityAId],
foreignColumns: [entityA.tenantId, entityA.projectId, entityA.id],
name: 'entity_a_entity_b_relations_a_fk',
}).onDelete('cascade'),
foreignKey({
columns: [table.tenantId, table.projectId, table.entityBId],
foreignColumns: [entityB.tenantId, entityB.projectId, entityB.id],
name: 'entity_a_entity_b_relations_b_fk',
}).onDelete('cascade'),
// Optional: unique constraint
unique('entity_a_entity_b_unique').on(table.entityAId, table.entityBId),
]
);
外键规则
- 删除时 CASCADE:删除父项时自动删除子项
- 删除时 SET NULL:用于可选引用
- 仅限同一数据库内:manage 与 runtime 数据库之间不得设置外键
- 由应用强制执行:跨数据库引用由代码强制执行
迁移规则
⚠️ 重要规则:
- 绝不要手动编辑
drizzle/meta/中的文件 - 绝不要在现有迁移 SQL 文件应用后再编辑它们
- 绝不要手动删除迁移文件——请使用
pnpm db:drop - ✅ 可以在新生成的迁移首次应用前编辑它们
变更集要求
架构变更需要变更集:
pnpm bump minor --pkg agents-core "Add myNewTable for storing X"
以下情况使用 minor 版本:
- 新建表
- 新增必填列
- 破坏性架构变更
以下情况使用 patch 版本:
- 新增带默认值的可选列
- 新建索引
- 非破坏性新增内容
数据模型变更检查清单
完成任何数据模型变更前,请确认:
- 架构定义在正确的文件中(manage 或 runtime)
- 使用了合适的作用域模式(tenant/project/agent/subAgent)
- JSONB 字段具有类型注解
- 主键包含所有作用域列
- 外键使用了正确的级联行为
- 已创建 Zod 架构(Select、Insert、Update、Api 变体)
- 已导出实体类型
- 已创建数据访问函数
- 已生成并审查迁移
- 如果 manage 架构有变更:已针对现有分支数据测试
pnpm --filter @inkeep/agents-core db:manage:sync-all-branches - 已创建变更集
- 已为新的数据访问函数编写测试