mcpField
D
data-model-changes

data-model-changes

github.com/inkeep数据与数据库1.5k49 次安装4 天前更新
代码仓库

关于 data-model-changes

为 Inkeep Agent Framework 贡献者提供数据模型变更指南,涵盖模式、验证、类型、数据访问和迁移,并区分版本化配置数据与运行时事务数据。还包括 Dolt 分支迁移检查。

主要功能

  1. 01Manage 与 runtime 数据库说明
  2. 02模式、关系和验证模式
  3. 03实体类型与数据访问步骤
  4. 04迁移生成及分支检查
  5. 05Changeset 与迁移安全规则

适用场景

  1. 01新增数据表或列
  2. 02扩展表关系和数据访问
  3. 03应用数据库模式迁移
  4. 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.tssrc/db/manage/manage-schema.ts版本化配置:项目、Agent、工具、触发器、评估器
Runtime(Postgres)drizzle.run.config.tssrc/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),
  ]
);

外键规则

  1. 删除时 CASCADE:删除父项时自动删除子项
  2. 删除时 SET NULL:用于可选引用
  3. 仅限同一数据库内:manage 与 runtime 数据库之间不得设置外键
  4. 由应用强制执行:跨数据库引用由代码强制执行

迁移规则

⚠️ 重要规则:

  • 绝不要手动编辑 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
  • 已创建变更集
  • 已为新的数据访问函数编写测试

评论

支持 Markdown