data-model-changes

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data Model Change Guide

数据模型变更指南

Comprehensive guidance for making changes to the data model (database schema, validation, types, and data access layer) in the Inkeep Agent Framework.

本文档为Inkeep Agent Framework中数据模型(数据库架构、验证规则、类型定义及数据访问层)的变更提供全面指导。

Database Architecture

数据库架构

The framework uses two separate PostgreSQL databases:
DatabaseConfig FileSchema FilePurpose
Manage (Doltgres)
drizzle.manage.config.ts
src/db/manage/manage-schema.ts
Versioned config: projects, agents, tools, triggers, evaluators
Runtime (Postgres)
drizzle.run.config.ts
src/db/runtime/runtime-schema.ts
Transactional data: conversations, messages, tasks, API keys
Key Distinction:
  • Manage DB: Configuration that changes infrequently (agent definitions, tool configs). Supports Dolt versioning.
  • Runtime DB: High-frequency transactional data (conversations, messages). No cross-DB foreign keys to manage tables.

本框架使用两个独立的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数据库:变更频率较低的配置信息(Agent定义、工具配置)。支持Dolt版本控制。
  • Runtime数据库:高频事务型数据(对话、消息)。与Manage表之间无跨数据库外键关联。

Schema Patterns

架构模式

1. Scope Patterns (Multi-tenancy)

1. 作用域模式(多租户)

All tables use hierarchical scoping. Use these reusable field patterns:
typescript
// 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(),
};
Example usage in real tables:
typescript
// 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] }),
  ]
);
所有表均采用分层作用域,可复用以下字段模式:
typescript
// 租户级(组织范围内的资源)
const tenantScoped = {
  tenantId: varchar('tenant_id', { length: 256 }).notNull(),
  id: varchar('id', { length: 256 }).notNull(),
};

// 项目级(项目专属资源)
const projectScoped = {
  ...tenantScoped,
  projectId: varchar('project_id', { length: 256 }).notNull(),
};

// Agent级(Agent专属资源)
const agentScoped = {
  ...projectScoped,
  agentId: varchar('agent_id', { length: 256 }).notNull(),
};

// 子Agent级(子Agent专属资源)
const subAgentScoped = {
  ...agentScoped,
  subAgentId: varchar('sub_agent_id', { length: 256 }).notNull(),
};
在实际表中的使用示例:
typescript
// 项目作用域:工具属于某个项目
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作用域:触发器属于某个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] }),
  ]
);

// 子Agent作用域:工具关联关系属于某个子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. Common Field Patterns

2. 通用字段模式

typescript
// 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(),
};
Example usage:
typescript
// 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] }),
  ]
);
typescript
// 标准UI属性
const uiProperties = {
  name: varchar('name', { length: 256 }).notNull(),
  description: text('description'),
};

// 标准时间戳
const timestamps = {
  createdAt: timestamp('created_at', { mode: 'string' }).notNull().defaultNow(),
  updatedAt: timestamp('updated_at', { mode: 'string' }).notNull().defaultNow(),
};
使用示例:
typescript
// 包含UI属性的表(面向用户的实体)
export const projects = pgTable(
  'projects',
  {
    ...tenantScoped,
    ...uiProperties,  // name(必填)、description(可选)
    models: jsonb('models').$type<ProjectModels>(),
    ...timestamps,    // createdAt、updatedAt
  },
  (table) => [primaryKey({ columns: [table.tenantId, table.id] })]
);

// 无UI属性的表(内部/关联表)
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,  // 仍需包含时间戳用于审计
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.agentId, table.id] }),
  ]
);

3. JSONB Type Annotations

3. JSONB类型注解

Always annotate JSONB columns with
.$type<T>()
for type safety:
typescript
models: jsonb('models').$type<Models>(),
config: jsonb('config').$type<{ type: 'mcp'; mcp: ToolMcpConfig }>().notNull(),
metadata: jsonb('metadata').$type<ConversationMetadata>(),

为保证类型安全,所有JSONB列必须使用
.$type<T>()
进行注解:
typescript
models: jsonb('models').$type<Models>(),
config: jsonb('config').$type<{ type: 'mcp'; mcp: ToolMcpConfig }>().notNull(),
metadata: jsonb('metadata').$type<ConversationMetadata>(),

Adding a New Table

新增表

Step 1: Define the Table in Schema

步骤1:在架构中定义表

Location:
packages/agents-core/src/db/manage/manage-schema.ts
(config) or
runtime-schema.ts
(runtime)
typescript
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),
  ]
);
位置:
packages/agents-core/src/db/manage/manage-schema.ts
(配置类)或
runtime-schema.ts
(运行时类)
typescript
export const myNewTable = pgTable(
  'my_new_table',
  {
    ...projectScoped,  // 选择合适的作用域
    ...uiProperties,   // 如果包含名称/描述
    
    // 自定义字段
    status: varchar('status', { length: 50 }).notNull().default('active'),
    config: jsonb('config').$type<MyConfigType>(),
    
    // 引用字段(可选)
    parentId: varchar('parent_id', { length: 256 }),
    
    ...timestamps,
  },
  (table) => [
    // 主键 - 必须包含所有作用域字段
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
    
    // 外键(仅允许同一数据库内!)
    foreignKey({
      columns: [table.tenantId, table.projectId],
      foreignColumns: [projects.tenantId, projects.id],
      name: 'my_new_table_project_fk',
    }).onDelete('cascade'),
    
    // 可选:为高频查询创建索引
    index('my_new_table_status_idx').on(table.status),
  ]
);

Step 2: Add Relations (if needed)

步骤2:添加关联关系(如有需要)

typescript
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
}));
typescript
export const myNewTableRelations = relations(myNewTable, ({ one, many }) => ({
  project: one(projects, {
    fields: [myNewTable.tenantId, myNewTable.projectId],
    references: [projects.tenantId, projects.id],
  }),
  // 根据需要添加更多关联
}));

Step 3: Create Zod Validation Schemas

步骤3:创建Zod验证Schema

Location:
packages/agents-core/src/validation/schemas.ts
typescript
// 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');
位置:
packages/agents-core/src/validation/schemas.ts
typescript
// 从Drizzle表创建基础Schema
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 Schema(省略内部作用域字段)
export const MyNewTableApiSelectSchema = createApiSchema(MyNewTableSelectSchema)
  .openapi('MyNewTableApiSelect');

export const MyNewTableApiInsertSchema = createApiInsertSchema(MyNewTableInsertSchema)
  .openapi('MyNewTableApiInsert');

export const MyNewTableApiUpdateSchema = createApiUpdateSchema(MyNewTableUpdateSchema)
  .openapi('MyNewTableApiUpdate');

Step 4: Create Entity Types

步骤4:创建数据访问函数

Location:
packages/agents-core/src/types/entities.ts
typescript
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>;
位置:
packages/agents-core/src/data-access/manage/myNewTable.ts
(或
runtime/
目录下)
typescript
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)
      )
    );
  };

Step 5: Create Data Access Functions

步骤5:从数据访问索引文件导出

Location:
packages/agents-core/src/data-access/manage/myNewTable.ts
(or
runtime/
)
typescript
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)
      )
    );
  };
位置:
packages/agents-core/src/data-access/index.ts
typescript
export * from './manage/myNewTable';

Step 6: Export from Data Access Index

步骤6:生成并应用迁移

Location:
packages/agents-core/src/data-access/index.ts
typescript
export * from './manage/myNewTable';
bash
undefined

Step 7: Generate and Apply Migration

生成迁移SQL

bash
undefined
pnpm db:generate

Generate migration SQL

查看drizzle/manage/或drizzle/runtime/目录下生成的SQL

如有需要可对新生成的文件进行小幅修改

应用迁移

pnpm db:generate
pnpm db:migrate

---

Review generated SQL in drizzle/manage/ or drizzle/runtime/

为现有表添加列

Make minor edits if needed (ONLY to newly generated files)

步骤1:修改架构

Apply migration

pnpm db:migrate

---
在现有表定义中添加新列:
typescript
// 在manage-schema.ts或runtime-schema.ts中
export const existingTable = pgTable(
  'existing_table',
  {
    // ... 现有字段 ...
    
    // 新列
    newField: varchar('new_field', { length: 256 }),
    newJsonField: jsonb('new_json_field').$type<MyNewType>().default(null),
    
    ...timestamps,
  },
  // ... 约束条件 ...
);

Adding a Column to Existing Table

步骤2:更新Zod Schema(如需自定义验证)

Step 1: Modify Schema

Add the new column to the table definition:
typescript
// 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 ...
);
如果该字段需要超出Drizzle默认规则的自定义验证:
typescript
// 在schemas.ts中,根据需要更新字段Schema
registerFieldSchemas(existingSchema, {
  newField: (schema) => schema.min(1).max(100),
});

Step 2: Update Zod Schema (if custom validation needed)

步骤3:生成并应用迁移

If the field needs custom validation beyond Drizzle defaults:
typescript
// In schemas.ts, update field schemas if needed
registerFieldSchemas(existingSchema, {
  newField: (schema) => schema.min(1).max(100),
});
bash
pnpm db:generate

Step 3: Generate and Apply Migration

查看生成的迁移SQL

bash
pnpm db:generate
pnpm db:migrate

---

Review the generated migration SQL

添加表之间的关联关系

多对多关联的中间表

pnpm db:migrate

---
typescript
export const entityAEntityBRelations = pgTable(
  'entity_a_entity_b_relations',
  {
    ...projectScoped,  // 选择合适的作用域
    entityAId: varchar('entity_a_id', { length: 256 }).notNull(),
    entityBId: varchar('entity_b_id', { length: 256 }).notNull(),
    // 可选:关联关系专属字段
    config: jsonb('config').$type<RelationConfig>(),
    ...timestamps,
  },
  (table) => [
    primaryKey({ columns: [table.tenantId, table.projectId, table.id] }),
    // 关联两个表的外键
    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'),
    // 可选:唯一约束
    unique('entity_a_entity_b_unique').on(table.entityAId, table.entityBId),
  ]
);

Adding Relations Between Tables

外键规则

Join Tables for Many-to-Many

typescript
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. 应用层强制约束:跨数据库引用需在代码中实现校验

Foreign Key Rules

迁移规则

  1. CASCADE on delete: Parent deletion removes children automatically
  2. SET NULL on delete: Use for optional references
  3. Within same database only: No FKs between manage and runtime DBs
  4. Application-enforced: Cross-DB references are enforced in code

⚠️ 核心规则:
  • 禁止手动编辑
    drizzle/meta/
    目录下的文件
  • 禁止修改已应用的迁移SQL文件
  • 禁止手动删除迁移文件 - 请使用
    pnpm db:drop
    命令
  • 允许在首次应用前编辑新生成的迁移文件

Migration Rules

变更集要求

⚠️ Critical Rules:
  • NEVER manually edit files in
    drizzle/meta/
  • NEVER edit existing migration SQL files after they've been applied
  • NEVER manually delete migration files - use
    pnpm db:drop
  • OK to edit newly generated migrations before first application

架构变更需要创建变更集:
bash
pnpm bump minor --pkg agents-core "Add myNewTable for storing X"
以下场景使用minor版本:
  • 新增表
  • 新增必填列
  • 破坏性架构变更
以下场景使用patch版本:
  • 新增带默认值的可选列
  • 新增索引
  • 非破坏性新增

Changeset Requirements

数据模型变更检查清单

Schema changes require a changeset:
bash
pnpm bump minor --pkg agents-core "Add myNewTable for storing X"
Use minor version for:
  • New tables
  • New required columns
  • Breaking schema changes
Use patch version for:
  • New optional columns with defaults
  • New indexes
  • Non-breaking additions

完成任何数据模型变更前,请确认:
  • 架构定义在正确的文件中(Manage或Runtime)
  • 使用了合适的作用域模式(租户/项目/Agent/子Agent)
  • JSONB字段已添加类型注解
  • 主键包含所有作用域列
  • 外键使用了正确的级联行为
  • 已创建Zod Schema(Select、Insert、Update、Api变体)
  • 已导出实体类型
  • 已创建数据访问函数
  • 已生成并审核迁移文件
  • 已创建变更集
  • 已为新数据访问函数编写测试用例

Checklist for Data Model Changes

Before completing any data model change, verify:
  • Schema defined in correct file (manage vs runtime)
  • Appropriate scope pattern used (tenant/project/agent/subAgent)
  • JSONB fields have type annotations
  • Primary key includes all scope columns
  • Foreign keys use correct cascade behavior
  • Zod schemas created (Select, Insert, Update, Api variants)
  • Entity types exported
  • Data access functions created
  • Migration generated and reviewed
  • Changeset created
  • Tests written for new data access functions