{
  "id": "ai-rules-for-postgresql-apps",
  "type": "rules",
  "category": "rules",
  "locale": "zh",
  "url": "/zh/rules/ai-rules-for-postgresql-apps",
  "title": "PostgreSQL应用的AI编码规则",
  "description": "AGENTS.md规则，适用于PostgreSQL后端应用，涵盖查询安全、迁移规范、索引约定，以及防止代理编写N+1查询。",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "PostgreSQL",
    "TypeScript",
    "Next.js"
  ],
  "tags": [
    "agents-md",
    "postgres",
    "typescript",
    "security",
    "conventions"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "将此文件作为 `AGENTS.md` 放置在仓库根目录。它适用于任何以 PostgreSQL 作为主要数据存储的项目，无论通过 Prisma、Drizzle、`postgres.js` 还是原生 SQL 访问。\n\n## AGENTS.md\n\n```md title=\"AGENTS.md\"\n# Project Rules — PostgreSQL Apps\n\n## Hard rules — data safety\n- NEVER generate SQL that concatenates user input into a query string. Always use\n  parameterised queries (`$1`, `$2` placeholders) or the ORM's parameter binding.\n  SQL injection via agent-generated queries is a real attack vector.\n- NEVER run `DROP TABLE`, `TRUNCATE`, or `DELETE FROM <table>` without a `WHERE`\n  clause in application code. These must be wrapped in a transaction that can be\n  rolled back, and must require explicit developer confirmation before execution.\n- NEVER write a migration that removes a column without first deploying a version of\n  the application that stops reading that column. Column removal is a two-deploy\n  operation: (1) stop reading; (2) drop.\n- All schema changes must be applied via migration files (Drizzle Kit, Flyway,\n  `migrate`, or Prisma Migrate). Never apply schema changes by running `ALTER TABLE`\n  directly against production.\n\n## Query conventions\n- Every query that returns a list MUST have a `LIMIT`. There is no acceptable reason\n  for a user-facing endpoint to return an unbounded result set.\n- Use `EXPLAIN ANALYZE` to check query plans for new queries on tables with more than\n  10,000 rows before merging. Agents should output the plan in a comment block when\n  writing a non-trivial query.\n- Avoid `SELECT *`. Name every column you need. This makes queries self-documenting\n  and prevents accidental exposure of sensitive columns (hashed passwords, tokens).\n- N+1 patterns are forbidden. If you load a list of records and then query related\n  data per row in a loop, refactor to a single JOIN query or a batched `IN` query.\n- Use `JOIN` instead of correlated subqueries for relation lookups — correlated\n  subqueries run once per row and are almost always slower.\n\n## Indexing rules\n- Every foreign key column MUST have an index. PostgreSQL does not create these\n  automatically (unlike MySQL). Missing FK indexes cause sequential scans on join.\n- Columns used in `WHERE` clauses on high-read tables should have indexes. When\n  adding a query that filters by a new column, check whether an index exists and\n  add one in the same migration if it does not.\n- Use partial indexes (`WHERE deleted_at IS NULL`) for soft-delete patterns — they\n  are far smaller and faster than full-table indexes when most rows are soft-deleted.\n- Never create an index on a column with very low cardinality (e.g. a boolean\n  `is_active` with 90% `true`). PostgreSQL will ignore the index and scan anyway.\n\n## Connection and pooling\n- Import the database client from `src/lib/db.ts` (the singleton or pool). Never\n  instantiate a new client per request — this exhausts connections instantly under load.\n- In serverless environments (Next.js API routes, Edge Functions), use a connection\n  pooler (PgBouncer, Neon connection pooling, Supabase pgbouncer mode) between the\n  application and PostgreSQL. Direct connections from serverless are not sustainable.\n- Set `statement_timeout` and `lock_timeout` on the connection or session for\n  long-running operations to prevent them from blocking the entire application.\n\n## Transactions\n- Any operation that writes to more than one table must be wrapped in a transaction.\n  Partial writes leave the database in an inconsistent state.\n- Keep transactions as short as possible. Do not make network requests (HTTP calls,\n  external APIs) inside a transaction — this holds locks for the duration of the\n  network call.\n\n## Definition of done\n- No string-interpolated SQL in the codebase (`grep -r \"query\\`\" src/` should return\n  only parameterised template tag usage, not concatenation).\n- All new tables have primary key and foreign key indexes.\n- All `findMany` / `SELECT` queries have `LIMIT`.\n- Migration files are committed and named descriptively.\n- `tsc --noEmit` passes (typed query results match schema types).\n```\n\n## 为什么需要这些规则\n\n- **永远不要字符串拼接 SQL** 是基础规则。参数化查询不仅是最佳实践，更是防御 SQL 注入的唯一手段——代理在时间压力下，如果不知道如何用特定 ORM 的参数语法表达查询，就会进行拼接。\n- **删除列需要两次部署** 避免了最常见的零停机迁移错误。如果迁移删除列时应用程序仍在读取该列，那么迁移时刻所有正在处理的请求都会失败。代理在生成模式变更时，不会自然地考虑到部署窗口。\n\n## 适用场景\n\n- 任何使用 PostgreSQL 的生产应用程序，且数据完整性和查询性能至关重要：SaaS 应用、内容平台、电商后端、分析管道。\n\n## 不适用场景\n\n- 基于 SQLite 的应用或数据库真正临时的项目（测试夹具、本地开发种子数据）——迁移规范和索引规则是不必要的开销。"
}