一键导入
drizzle-orm
Use when writing Drizzle ORM schemas, migrations, queries, or debugging Drizzle type errors in TypeScript projects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing Drizzle ORM schemas, migrations, queries, or debugging Drizzle type errors in TypeScript projects.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when designing functions, modeling data, choosing types, drawing module boundaries, or deciding what depends on what. Use when evaluating architecture, extracting abstractions, or shaping vertical slices.
Use when writing, reviewing, or refactoring code in any language. Use for architecture decisions, system design, component boundaries, and code quality judgment. Always relevant when touching source code.
Use when writing or reviewing comments, docstrings, names, control flow, or file organization. Use when evaluating readability, choosing identifiers, splitting files, or applying naming conventions. Use when removing AI tells (slop) from code prose — comments, docs, error messages, commit messages, PR descriptions. Covers the visible surface of code.
Run kirby's review engine on a PR or a commit, OUTSIDE the orchestrator loop. Local output only — findings are presented in-conversation, nothing is posted, no Provider config needed.
Launch kirby-bot orchestrator in background, relay phase transitions live from run.jsonl.
Extract and organize existing code into reusable modules, functions, and components with thoughtful APIs.
| name | drizzle-orm |
| description | Use when writing Drizzle ORM schemas, migrations, queries, or debugging Drizzle type errors in TypeScript projects. |
primaryKey, never an array of FK ids on either side.db.query.X.findMany/findFirst({ with: { ... } })? → the referenced relations MUST be declared: import { relations } + exported xRelations/yRelations on BOTH sides. with cannot resolve at runtime without them.Pool + drizzle() created ONCE at module top-level (outside the handler), max: 1. Never instantiate them inside the handler, never pool.end() per request.json('col').$type<T>() for typed JSON columns, never leave as unknown/anytypeof table.$inferSelect and $inferInsert for types (not InferSelectModel)productId: integer('product_id')timestamp('col', { withTimezone: true }) not bare timestamp('col'). Without timezone, timestamps are ambiguous when servers/clients are in different zones. Add .defaultNow() for createdAt, use .$onUpdate(() => new Date()) for updatedAtschema/index.ts barrel file. Import the full schema object into drizzle() config: drizzle(client, { schema }). Relations must reference tables from the same schema object — split files work only if re-exported togetherproductId, userId, anything ending in Id), it MUST have an index in the table-config callback. No exceptions. WHY: FKs are joined and filtered constantly; without an index every join is a full table scan. Pattern: pgTable('orders', { ...cols }, (t) => ({ productIdx: index('product_idx').on(t.productId), userIdx: index('user_idx').on(t.userId) })). Also index frequently queried columnsexport const productsToCategories = pgTable('products_to_categories', { productId: integer('product_id').references(() => products.id), categoryId: integer('category_id').references(() => categories.id) }, (t) => ({ pk: primaryKey({ columns: [t.productId, t.categoryId] }) })). The composite primaryKey is required — it prevents duplicate pairs. WHY: a single FK column can only model one-to-many; M:N has no home on either tabledb.query.X.findMany({ with: { ... } }) for relational queries, not manual joins. But with resolves nothing unless the relations are declared: every table referenced in a with clause MUST have an exported relations() definition on BOTH sides, and the schema object passed to drizzle(client, { schema }) must include them. Review checklist: if you write db.query.products.findMany({ with: { orders } }), you MUST also import { relations } from 'drizzle-orm' and export productsRelations/ordersRelations, else the query is non-functional. Pattern: export const productsRelations = relations(products, ({ many }) => ({ orders: many(orders) })); export const ordersRelations = relations(orders, ({ one }) => ({ product: one(products, { fields: [orders.productId], references: [products.id] }) }));json_agg/array_agg, count, ordering, filtering, deduplication, group-by) must be in the query, not rebuilt in TS with Map/for/.filter().map(). Fetching flat rows just to reshape them is slower than the DB and a vector for off-by-one bugs. Prefer db.query.X.findMany({ with: { ... } }) with explicit defineRelations, or a single select with explicit joins/aggregates. If you find yourself rebuilding parent/child relations in code, the schema lacks a relation declaration — fix the schema, not the handler. Only acceptable in-code post-processing: final domain transformation (DB row → API contract), never the join itself. Reviews: handler fetching flat rows then reshaping them in JS to nest children under parents -> flag "move the join into SQL"gt(t.id, lastSeenId) for large datasets, not OFFSET.returning() on every insert/update — always chain .returning() after insert/update to get the created/modified row back in one round-trip instead of a separate SELECT. db.insert(users).values({...}).returning() returns the full row with generated id, createdAt, etc. Without it you waste a round-trip on a follow-up SELECT, and risk reading stale data under concurrencydeletedAt: timestamp('deleted_at') column, filter with isNull(t.deletedAt) on every query. WHY: hard deletes break FK integrity and lose audit trail. Create a helper withActive(query) to avoid forgetting the filter — one missed isNull and you leak "deleted" data to users.onConflictDoUpdate({ target: t.email, set: { name: sqlexcluded.name, updatedAt: new Date() } }) after .insert().values(). Use onConflictDoNothing() when you only care about existence (idempotent inserts). The excluded keyword references the row that would have been inserteddb.insert(t).values(arrayOfRows).returning(). For large batches (1000+), chunk into groups of 500 — most DBs have a parameter limit (~65535 for Postgres). A 10-column table hits this at ~6500 rows. Chunk to be safetenantId column to all tenant-scoped tables + composite indexes. Create a helper withTenant(tenantId) that wraps queries with and(eq(t.tenantId, tenantId), ...). Never rely on application-level filtering alone — a missed where clause leaks cross-tenant datadb.transaction() for ALL multi-step data modificationsdrizzle-kit push for development, generate+migrate for production — push applies schema diff directly (fast iteration, no migration files). generate creates SQL migration files for reviewable, reproducible production deploys. Never use push in production — it can drop columns with data} satisfies Config not const config: Config =db.select() bare — always pass a column list — review checklist: every db.select() with empty parens is a violation; replace with db.select({ id: t.id, name: t.name }) listing only the columns the caller uses. This is not "for large tables only" — do it on every select. Before: db.select().from(products). After: db.select({ id: products.id, name: products.name }).from(products). WHY: bare select fetches every column including large/unused ones, wasting bandwidth and coupling the query to schema changesgetAll query MUST be paginated — there is no unbounded fetch — review checklist: if a function returns a list (named getAll*, list*, findMany, or any .from(t) without a limit), it MUST have .limit() plus cursor (gt(t.id, lastSeenId)) or offset. A function named getAllProducts is a trap, not a license to fetch everything — rename to getProducts(cursor) and add .limit(N). Before: db.select({...}).from(products). After: db.select({...}).from(products).where(gt(products.id, cursor)).limit(50). WHY: an unbounded fetch grows with the table and will eventually OOM or time out in productionmax: 1 — review checklist: in a serverless/edge handler (Neon/Supabase/Lambda/Vercel) the Pool and drizzle() MUST be created once at module top-level (outside the handler) so they are reused across warm invocations, and new Pool({...}) MUST set max: 1. Two violations to catch: (1) new Pool()/drizzle() instantiated INSIDE the handler — rebuilds the pool every request; (2) pool.end() called per invocation — tears down the connection you just paid to open and breaks reuse. Each serverless instance opens its own pool; without max: 1 you exhaust the database's connection limit under load. Pattern: const pool = new Pool({ connectionString, max: 1, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000 }); const db = drizzle(pool, { schema }); export async function handler(event) { /* use db, no end() */ }. Long-running servers only: max: (cpu_cores * 2) + 1. Always set idleTimeoutMillis (30s) and connectionTimeoutMillis (5s) to prevent connection leakscreateInsertSchema(table) and createSelectSchema(table) from drizzle-zod to auto-generate Zod schemas from your Drizzle table. WHY: single source of truth — schema changes propagate to validation automatically, no manual sync between DB schema and API validationcreateInsertSchema(users).omit({ id: true, createdAt: true }) — strip auto-generated fields that clients should never sendconst input = createInsertSchema(users).omit({ id: true }).parse(req.body) — validates AND types in one stepsql template tag with user input, always use sql.placeholder('name') for prepared statements or the tagged template literal. NEVER sql.raw(userInput) — it bypasses parameterization and enables SQL injection