跳到正文
cccskills
免费在 GitHub 公开中文介绍

drizzle

本技能提供使用 Drizzle ORM 定义数据库结构和编写查询的规范,涵盖表与列命名、主外键、索引、时间戳和 JSONB 类型,并介绍显式联接、聚合及 CTE 查询。

原文查看原文说明

Use for Drizzle schemas and queries: tables, indexes, relations, joins and inferred types. Rollout belongs to db-migrations.

lobehub/lobehub星标 83,506(GitHub 83,506 + cccskills 0)
集成工具PostgreSQL

实测评分 未实测

查看安装方法

适用场景

  • 定义新表或索引
  • 编写带联接或聚合的类型化查询
  • 按规范检查 Drizzle 结构或查询

中文介绍

功能

本技能介绍如何使用 Drizzle ORM 定义数据库结构和编写查询,涵盖表与列的命名、主键和外键、索引、时间戳以及 JSONB 类型。对于新表,建议使用单列主键,并通过唯一索引表达业务上的唯一性。查询通常采用 db.select() 构建器,也提供联接、聚合和 CTE 的示例。对于递归 CTE 等难以用 Drizzle 表达的情况,技能说明了如何在保留结构引用的同时使用原始 SQL。

适用场景

添加数据库模型或仓库、使新结构符合现有规范,或将使用 db.query.* 的代码改写为明确的 select 和联接时,可以参考本技能。迁移流程不在范围内,相关内容请查看 db-migrations 技能。

使用示例

  • “请按现有命名和主键规范检查这个表定义。”
  • “请用 select 构建器和明确的联接改写这个 Drizzle 查询。”

注意事项

新增模型或仓库时,需在同一变更中加入相应测试。

本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。

文档检查

AI 根据 SKILL.md 的内容检查了需要注意的操作和使用前提。评分只依据实际运行的结果(实测对比)。

安全性发送数据、删除文件、执行命令等是否有说明
A良好所给资料没有执行、发送或删除操作步骤。
安装难易度所需工具和 API 密钥是否少且有说明
A良好资料提到 Drizzle ORM,没有列出其他安装前提。

不计入评分。 由 AI(OpenAI Codex)于 2026年10月26日 评估。仅供参考,安装前请同时查看原文。

如果您认为评估有误,请申请复核

请写明哪一项有误以及原因。无需登录。我们会对照 SKILL.md 重新评估,并在此处更新结果。

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

包含的文件(1)

  • SKILL.md16.2 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Drizzle ORM Schema Style Guide

Adding a Model or Repository? Ship a sibling test in the same PR — every new file under packages/database/src/models/** or src/repositories/** needs a matching __tests__/<name>.test.ts. See the testing skill (.agents/skills/testing/references/db-model-test.md) for the getTestDB() integration pattern, user-isolation tests, the BM25 describe.skipIf(!isServerDB) guard, and schema gotchas. CI's coverage patch gate won't reliably catch a brand-new untested file, so this is on you.

Configuration

  • Config: drizzle.config.ts
  • Schemas: packages/database/src/schemas/
  • Migrations: packages/database/migrations/
  • Dialect: postgresql with strict: true

Helper Functions

Location: packages/database/src/schemas/_helpers.ts

  • timestamptz(name): Timestamp with timezone
  • createdAt(), updatedAt(), accessedAt(): Standard timestamp columns
  • timestamps: Object with all three for easy spread

Naming Conventions

  • Tables: Plural snake_case (users, session_groups)
  • Columns: snake_case (user_id, created_at)
  • New tables: Check nearby existing tables before naming a new one. Preserve the established noun family and suffix. For example, if the user-scoped table is user_xxx_logs, the workspace-scoped counterpart should be workspace_xxx_logs, not workspace_xxx_records or another new synonym.
// ✅ Good: follows the existing user/workspace table family (names are
// illustrative — swap `widget` for the real noun).
export const userWidgetLogs = pgTable('user_widget_logs', { ... });
export const workspaceWidgetLogs = pgTable('workspace_widget_logs', { ... });

// ❌ Bad: introduces a new suffix for the same concept.
export const workspaceWidgetRecords = pgTable('workspace_widget_records', { ... });

Column Definitions

Primary Keys

Do not use auto-incrementing primary keys (serial, bigserial, generated identity columns). They create sequence-state problems during cross-database migrations, restores, and data copy jobs. Prefer text IDs from application generators (idGenerator, createNanoId) or uuid for internal tables.

Keep $defaultFn(...) when a table normally owns ID generation. Callers can still pass an explicit id; the default only runs when the insert omits it. Do not remove the default just because one flow needs to supply a request-scoped ID.

// ✅ Good: app-generated text ID; explicit inserts can still override it.
id: text('id')
  .primaryKey()
  .$defaultFn(() => idGenerator('agents'))
  .notNull(),

// ❌ Bad: sequence state is fragile across DB migrations and restores.
id: serial('id').primaryKey(),

ID prefixes make entity types distinguishable. For internal tables, use uuid.

Do not use composite primary keys on new tables. Give every table a single-column surrogate PK and carry business uniqueness in a uniqueIndex instead. PK columns cannot be nullable, so when the uniqueness scope later grows by a nullable dimension the composite PK must be torn down and rebuilt — exactly what happened when ai_providers / ai_models were workspace-scoped (migrations 0110–0111 replaced their composite PKs with a surrogate _id plus partial unique indexes). A unique index still works as the arbiter for onConflictDoUpdate upserts.

// ✅ Good: surrogate PK; uniqueness scope can evolve without a PK rebuild.
export const workspaceUserSettings = pgTable(
  'workspace_user_settings',
  {
    id: uuid('id').defaultRandom().notNull().primaryKey(),
    workspaceId: text('workspace_id').references(() => workspaces.id, { onDelete: 'cascade' }).notNull(),
    userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
    ...timestamps,
  },
  (t) => [uniqueIndex('workspace_user_settings_workspace_id_user_id_unique').on(t.workspaceId, t.userId)],
);

// ❌ Bad: locked to exactly these columns; adding a nullable scope column
// (workspaceId, deviceId, …) later forces a full PK rebuild migration.
(t) => [primaryKey({ columns: [t.workspaceId, t.userId] })],

Existing composite PKs are legacy — leave them alone unless they block a scope change, then migrate them the 0110–0111 way.

Foreign Keys

userId: text('user_id')
  .references(() => users.id, { onDelete: 'cascade' })
  .notNull(),

Timestamps

...timestamps,  // Spread from _helpers.ts

Optional and Undefined Values

Do not introduce artificial sentinel strings for missing values, such as unknown, unless the domain already has that explicit state and existing code uses it consistently. Prefer nullable columns, optional TypeScript fields, or a separate concrete status enum when the value is genuinely absent.

// ✅ Good: absent until a custom-execution attempt actually runs
// (packages/database/src/schemas/agentIntervention.ts).
export const AGENT_INTERVENTION_CUSTOM_EXECUTION_STATES = ['pending', 'executing', 'completed'] as const;
export type AgentInterventionCustomExecutionState =
  (typeof AGENT_INTERVENTION_CUSTOM_EXECUTION_STATES)[number];

customExecutionState: text('custom_execution_state').$type<AgentInterventionCustomExecutionState>(),

// ❌ Bad: invents a new state that callers now need to handle everywhere.
export type AgentInterventionCustomExecutionState = 'pending' | 'executing' | 'completed' | 'unknown';

customExecutionState: text('custom_execution_state')
  .$type<AgentInterventionCustomExecutionState>()
  .notNull()
  .default('unknown');

Database Enums

Default to not using PostgreSQL/Drizzle pgEnum. Database enums are expensive to evolve safely: adding members needs migrations, removing or renaming members is awkward, and deployment order becomes more fragile.

For product/business states, use text() or varchar() with a TypeScript value type via $type<...>(). Keep those TS-only value types in the domain/shared type module, then import them into the schema. For cloud DB schemas, that usually means cloudDB/types.ts.

Do not copy existing DB enums as a pattern. Treat them as legacy or explicitly reviewed exceptions. If a new pgEnum seems necessary, stop and justify why the value set is effectively immutable and why the migration cost is acceptable.

Field Descriptions

For columns whose meaning is not obvious from the name alone, add JSDoc on the schema field. Include a concrete example when it clarifies the stored value or the lifecycle moment that writes it. This is especially important for external IDs, lifecycle statuses, denormalized snapshots, JSONB signals, and fields whose name could mean either a request ID or a persisted row ID.

// ✅ Good: explain the table's business object first, then only document
// non-obvious lifecycle fields
// (packages/database/src/schemas/agentIntervention.ts).
/**
 * Private resolution/outbox records. Unlike `agent_interventions`, this table
 * may retain user-edited arguments so a claimed decision can be delivered
 * reliably after the app disconnects. It is never a notification payload.
 */
export const agentInterventionResolutions = pgTable('agent_intervention_resolutions', {
  /** Owner of the durable operation, distinct from the resolving actor. */
  userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),

  /** Private executor result; never selected into Review or notification DTOs. */
  customExecutionResult: jsonb('custom_execution_result').$type<AgentInterventionCustomExecutionResult>(),

  /** Monotonic lease attempt, starting at zero while pending. */
  customExecutionAttempt: integer('custom_execution_attempt'),
});

// ❌ Bad: comments restate obvious column names without adding domain meaning.
/** User email */
email: text('email'),

JSONB Types

Avoid Record<string, unknown> or similarly loose JSONB types for schema columns. Define a concrete interface that describes the expected JSON shape, even when most properties are optional. This keeps callers, migrations, and review queries aligned on the same data contract.

// packages/types/src/agent/agentIntervention.ts +
// packages/database/src/schemas/agentIntervention.ts
interface AgentInterventionCustomExecutionResult {
  content: string;
  pluginState: Record<string, unknown>;
}

customExecutionResult: jsonb('custom_execution_result').$type<AgentInterventionCustomExecutionResult>(),
// ❌ Bad: hides the contract and makes downstream access untyped.
customExecutionResult: jsonb('custom_execution_result').$type<Record<string, unknown>>(),

A loosely-typed JSONB column is often a symptom of a deeper problem: the column was reserved speculatively ("for future extension") and nothing actually writes it. Don't add metadata / extra JSONB columns for hypothetical future needs — a column earns its place only when a concrete writer ships alongside it. When review finds such a column, the fix is to delete the column, not to invent an interface for data that doesn't exist; add a properly-typed column once the real requirement arrives.

Indexes

// Return array (object style deprecated)
(t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],

Type Inference

export const insertAgentSchema = createInsertSchema(agents);
export type NewAgent = typeof agents.$inferInsert;
export type AgentItem = typeof agents.$inferSelect;

Example Pattern

export const agents = pgTable(
  'agents',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => idGenerator('agents'))
      .notNull(),
    slug: varchar('slug', { length: 100 })
      .$defaultFn(() => randomSlug(4))
      .unique(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' })
      .notNull(),
    clientId: text('client_id'),
    chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(),
    ...timestamps,
  },
  (t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);

Common Patterns

Junction Tables (Many-to-Many)

The surrogate-PK rule above applies to junction tables too — pair uniqueness goes in a uniqueIndex, not a composite PK (many existing junction tables still use composite PKs; that is legacy, not the template):

export const agentsKnowledgeBases = pgTable(
  'agents_knowledge_bases',
  {
    id: uuid('id').defaultRandom().notNull().primaryKey(),
    agentId: text('agent_id')
      .references(() => agents.id, { onDelete: 'cascade' })
      .notNull(),
    knowledgeBaseId: text('knowledge_base_id')
      .references(() => knowledgeBases.id, { onDelete: 'cascade' })
      .notNull(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' })
      .notNull(),
    enabled: boolean('enabled').default(true),
    ...timestamps,
  },
  (t) => [
    uniqueIndex('agents_knowledge_bases_agent_id_knowledge_base_id_unique').on(
      t.agentId,
      t.knowledgeBaseId,
    ),
  ],
);

Query Style

Always use db.select() builder API. Never use db.query.* relational API (findMany, findFirst, with:).

The relational API generates complex lateral joins with json_build_array that are fragile and hard to debug.

Select Single Row

// ✅ Good
const [result] = await this.db.select().from(agents).where(eq(agents.id, id)).limit(1);
return result;

// ❌ Bad: relational API
return this.db.query.agents.findFirst({
  where: eq(agents.id, id),
});

Select with JOIN

// ✅ Good: explicit select + leftJoin
const rows = await this.db
  .select({
    runId: agentEvalRunTopics.runId,
    score: agentEvalRunTopics.score,
    testCase: agentEvalTestCases,
    topic: topics,
  })
  .from(agentEvalRunTopics)
  .leftJoin(agentEvalTestCases, eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id))
  .leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id))
  .where(eq(agentEvalRunTopics.runId, runId))
  .orderBy(asc(agentEvalRunTopics.createdAt));

// ❌ Bad: relational API with `with:`
return this.db.query.agentEvalRunTopics.findMany({
  where: eq(agentEvalRunTopics.runId, runId),
  with: { testCase: true, topic: true },
});

Select with Aggregation

// ✅ Good: select + leftJoin + groupBy
const rows = await this.db
  .select({
    id: agentEvalDatasets.id,
    name: agentEvalDatasets.name,
    testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'),
  })
  .from(agentEvalDatasets)
  .leftJoin(agentEvalTestCases, eq(agentEvalDatasets.id, agentEvalTestCases.datasetId))
  .groupBy(agentEvalDatasets.id);

Raw SQL and Advanced Queries

Prefer Drizzle builders whenever the query reads clearly with select, insert().select(), update().from(), joins, CTEs, and groupBy — this keeps table/column references tied to schema, so changes surface as TypeScript errors. Within a builder, expression-level sql<T> is fine for features lacking a helper (JSON path, casts, aggregates, CASE, NOW()). Row locks are clauses, not expressions — use .for('update'), never raw FOR UPDATE.

Use COALESCE only when null-handling is part of required DB semantics (nullable JSONB append/merge, "keep first non-null"). Don't scatter COALESCE(excluded.col, current.col) across ordinary upsert scalars just to avoid an update object — build set from defined values only, and hide any remaining SQL behind named helpers (appendJsonbArray, mergeJsonbObject, keepFirstValue) so the method reads as business intent, not SQL plumbing.

// ✅ Scalars included only when present; SQL hidden behind a named helper.
// (`userWidgetLogs` is illustrative — swap for the real table.)
const updateValues = compactUndefined({
  email: record.email ?? undefined,
  ip: record.ip ?? undefined,
});
await db.insert(userWidgetLogs).values(values).onConflictDoUpdate({
  set: { ...updateValues, events: appendJsonbArray(userWidgetLogs.events, event), updatedAt: now },
  target: userWidgetLogs.id,
});

// ❌ Every scalar becomes SQL plumbing.
set: {
  email: sql`COALESCE(excluded.email, ${userWidgetLogs.email})`,
  ip: sql`COALESCE(excluded.ip, ${userWidgetLogs.ip})`,
}

When refactoring raw SQL:

  • Preserve query shape on latency-sensitive paths. If raw SQL is one roundtrip, don't split it into multiple depth-based queries just to drop execute.
  • Use $with(...) + insert().select() / update().from() for multi-step single-roundtrip writes Drizzle can express.
  • Don't rely on execute<MyRow>(sql...) for safety — it types rows but doesn't keep selected columns in sync with schema changes.
  • If only a PostgreSQL feature Drizzle can't express works, keep the raw SQL and tighten it: schema refs in interpolations, explicit user scope, a narrow row interface, and regression tests.

Recursive CTEs are the canonical "keep raw" case — there's no clean WITH RECURSIVE builder, and a rewrite would add depth-based roundtrips:

interface TaskTreeRow {
  id: string;
  parent_task_id: string | null;
}

// execute<T> acceptable: no clean WITH RECURSIVE builder. Keep schema refs in the
// interpolations and scope every leg to the user.
const { rows } = await db.execute<TaskTreeRow>(sql`
  WITH RECURSIVE task_tree AS (
    SELECT ${tasks.id}, ${tasks.parentTaskId}
    FROM ${tasks}
    WHERE ${tasks.id} = ${rootTaskId} AND ${tasks.createdByUserId} = ${userId}
    UNION ALL
    SELECT ${tasks.id}, ${tasks.parentTaskId}
    FROM ${tasks}
    JOIN task_tree ON ${tasks.parentTaskId} = task_tree.id
    WHERE ${tasks.createdByUserId} = ${userId}
  )
  SELECT * FROM task_tree
`);

One-to-Many (Separate Queries)

When you need a parent record with its children, use two queries instead of relational with::

// ✅ Good: two simple queries
const [dataset] = await this.db
  .select()
  .from(agentEvalDatasets)
  .where(eq(agentEvalDatasets.id, id))
  .limit(1);

if (!dataset) return undefined;

const testCases = await this.db
  .select()
  .from(agentEvalTestCases)
  .where(eq(agentEvalTestCases.datasetId, id))
  .orderBy(asc(agentEvalTestCases.sortOrder));

return { ...dataset, testCases };

Database Migrations

See the db-migrations skill for the detailed migration guide.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

acceptance

免费有简介

通过实际操作产品验证交付内容,记录用户可查看的证据,并发布验收报告。可检查已有验证计划或创建检查项,选择合适的操作界面,并使用 `lh` CLI 发布结果。

  • 实际检查更改后的产品行为
  • 收集截图或其他验证证据
  • 编写验证计划和报告
lobehub/lobehub8.4万2026年10月26日 更新

add-model-provider

免费有简介

本技能指导在 LobeHub 中接入 AI 模型提供商,涵盖运行时、模型信息、设置、品牌展示、文档和验收,并帮助判断新增模型是否需要新的提供商集成。

  • 为 LobeHub 接入新的 AI 模型提供商
  • 更新提供商设置说明或介绍
  • 判断添加模型是否需要新的提供商集成
lobehub/lobehub8.4万2026年10月26日 更新

add-setting-env

免费有简介

本技能指导如何用服务器端环境变量设置用户偏好默认值,涵盖变量校验、服务器配置、写入用户状态,以及更新 `.env.example` 和自托管文档。

  • 通过环境变量设置用户偏好默认值
  • 将设置接入服务器配置和用户状态
  • 更新环境变量示例及文档
lobehub/lobehub8.4万2026年10月26日 更新

agent-runtime-hooks

免费有简介

说明如何在智能体执行步骤、工具调用、人工审批、上下文压缩和子智能体调用时注册钩子。钩子可观察事件、模拟本地工具结果,也可通过 Webhook 按配置策略允许或拒绝工具调用。

  • 记录智能体步骤和错误
  • 测试时模拟工具结果
  • 通过 Webhook 允许或拒绝工具调用
lobehub/lobehub8.4万2026年10月26日 更新

agent-signal

免费有简介

此技能用于为智能体设计事件驱动的后台工作流,划分来源、信号、动作和策略的职责,并处理异步交接、去重、执行范围、完成记录与运行观测。

  • 添加事件触发的后台任务
  • 将工作移出对话请求
  • 防止动作重复执行
lobehub/lobehub8.4万2026年10月26日 更新

agent-testing-bot

免费有简介

本技能在 macOS 桌面聊天应用中测试机器人频道:打开应用、进入频道或联系人、发送消息、等待回复并截图。支持 Discord、Slack、Telegram、微信、飞书和 QQ;iMessage 使用 BlueBubbles 桥接。

  • 检查真实 Discord 频道中的机器人回复
  • 测试 Slack 或 Telegram 的对话流程
  • 记录 macOS 应用中的操作结果
lobehub/lobehub8.4万2026年10月26日 更新

查看 lobehub 的全部技能

报告此技能的问题