refactor(db): UUIDv7 \u751f\u6210\u4e0b\u63a8\u5230 PG18 \u539f\u751f uuidv7()\uff0c\u8005\u53ea\u6539 schema
\u53ea\u6539 schema \u5c42\u9762\uff1a - src/server/db/fields.ts: $defaultFn(() => Bun.randomUUIDv7()) \u2192 default(sql`uuidv7()`) - AGENTS.md Stack & runtime: \u52a0 PG18+ \u786c\u7ea6\u675f - AGENTS.md Drizzle \u8282\u8bf4\u660e DB-side uuidv7\uff08\u5355\u8c03\u3001\u4f7f\u7528 DB \u65f6\u949f\uff09 - AGENTS.md Bun-native \u539f\u5219\u533a\u5206 app-code UUIDv7 \u4e0e DB PK - AGENTS.md Don'ts \u9996\u6761\u52a0 "AI \u4e0d\u80fd\u8dd1 db:generate" \u4f9d\u7136\u9700\u8981\u4f60\u624b\u52a8\u8dd1 `bun run db:generate` \u4ee5\uff1a 1) \u751f\u6210\u65b0 migration\uff08\u5e94\u8be5\u662f DROP \u8001\u8868 + CREATE \u65b0\u8868\uff0c \u6216\u4f60\u624b\u5199 ALTER COLUMN id SET DEFAULT uuidv7()\uff09 2) \u91cd\u751f migrations.gen.ts \u672c commit \u72b6\u6001\u4e0b\u8fd0\u884c\u65f6\u5c1a\u4e0d\u53ef\u7528\uff08\u8001 migration \u672a\u8bbe DEFAULT\uff0c \u63d2\u5165\u4f1a\u62a5 NOT NULL \u9519\uff09\uff1bdb:generate \u540e\u91cd\u65b0 build/compile/deploy \u624d\u662f \u5b8c\u6574\u72b6\u6001\u3002fix / typecheck / test 3/3 \u5747\u8fc7\uff08\u9759\u6001\u68c0\u67e5\u4e0d\u4f9d\u8d56 migration\uff09\u3002
This commit is contained in:
@@ -5,9 +5,10 @@ Compact, repo-specific notes for AI agents. Generic language/framework knowledge
|
||||
## Stack & runtime
|
||||
|
||||
- **Bun-only** (`mise.toml` pins `bun = 1.3.13`). Never invoke `npm`/`npx`/`node`/`yarn`/`pnpm`. Use `bun run <script>` (bare `bun <script>` can collide with Bun built-in subcommands).
|
||||
- **Prefer Bun-native APIs over external packages and `node:*` polyfills.** UUIDv7 → `Bun.randomUUIDv7()` (not the `uuid` package); SHA-256 → `Bun.CryptoHasher.hash('sha256', s, 'hex')` (not `node:crypto.createHash`); short sleeps → `Bun.sleep(ms)` (not raw `setTimeout` with promise wrapping); file I/O in build scripts → `Bun.file` / `Bun.write` are fine. The runtime is Bun, the deployment target is Bun, the test runner is Bun — there is no "portability" concern that would justify dragging in npm packages or Node compat shims for things Bun ships natively.
|
||||
- **Prefer Bun-native APIs over external packages and `node:*` polyfills.** UUIDv7 in app code → `Bun.randomUUIDv7()` (not the `uuid` package); DB primary keys are a separate matter — those go through PG18's `uuidv7()`, see "Drizzle" section. SHA-256 → `Bun.CryptoHasher.hash('sha256', s, 'hex')` (not `node:crypto.createHash`); short sleeps → `Bun.sleep(ms)` (not raw `setTimeout` with promise wrapping); file I/O in build scripts → `Bun.file` / `Bun.write` are fine. The runtime is Bun, the deployment target is Bun, the test runner is Bun — there is no "portability" concern that would justify dragging in npm packages or Node compat shims for things Bun ships natively.
|
||||
- TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset `bun`). Vite dev port is **strict 3000**.
|
||||
- PostgreSQL + **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
||||
- **PostgreSQL 18+ only** (`compose.yaml` pins `postgres:18-alpine`). The starter relies on PG18's built-in `uuidv7()` function for primary-key generation — see "Drizzle" section. Do not soften this to support older PG; if you need PG <18 compatibility, fork and reintroduce app-side UUIDv7 (e.g. `Bun.randomUUIDv7()` or the `uuid` package) yourself.
|
||||
- **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
||||
- ORPC (contract-first), TanStack Query v5, Tailwind v4.
|
||||
- **Logging via [LogTape](https://logtape.org/)** (zero-dep, runtime-agnostic) — see "Logging" section. `console.*` is forbidden in business code.
|
||||
|
||||
@@ -47,7 +48,7 @@ Before committing: `bun run fix && bun run typecheck && bun run test`. No CI, no
|
||||
```
|
||||
Do NOT use the v2 object form (`orderBy: { createdAt: 'desc' }`, `where: { id }`) — it won't type-check.
|
||||
- To add relations later: declare per-table with `relations()` from `drizzle-orm` and export them from the same file as the table; they get picked up automatically because `index.ts` does `drizzle({ schema })` via `import *`.
|
||||
- Every table must spread `...generatedFields` from `src/server/db/fields.ts` (`id` UUIDv7 via `$defaultFn(uuidv7)`, `createdAt`, `updatedAt` with `$onUpdateFn`). `generatedFieldKeys` is hand-written and uses `satisfies Record<keyof typeof generatedFields, true>` so any field-key drift fails typecheck; it feeds `createInsertSchema(...).omit(...)` / `createUpdateSchema(...).omit(...)`.
|
||||
- Every table must spread `...generatedFields` from `src/server/db/fields.ts` (`id uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL` — **Postgres-side generation**, requires PG18+; `createdAt`, `updatedAt` with `$onUpdateFn`). The DB is the single source of UUIDv7 truth: monotonic per cluster, uses DB clock, no app-side round-trip. **Do not reintroduce `$defaultFn(() => Bun.randomUUIDv7())`** — the SQL default is what the migration emits and what `drizzle-zod` reads as "optional in insert schema". `generatedFieldKeys` is hand-written and uses `satisfies Record<keyof typeof generatedFields, true>` so any field-key drift fails typecheck; it feeds `createInsertSchema(...).omit(...)` / `createUpdateSchema(...).omit(...)`.
|
||||
- `src/server/db/index.ts` exports a module-level `const db = drizzle(...)` — not a lazy singleton. On Bun this is a long-lived process, so top-level side effects are fine and requested. Don't reintroduce `getDB/closeDB` ceremony; the Nitro shutdown plugin calls `db.$client.end()` directly. (Cloudflare Workers would need per-request init — we don't support that deployment target.)
|
||||
- `drizzle.config.ts` runs outside Vite — `@/*` path aliases do NOT resolve there. It currently does `import { env } from './src/env'` (relative). Preserve that.
|
||||
- **Migrations are embedded in the binary, not read from disk.** `bun run db:generate` chains `drizzle-kit generate && bun scripts/embed-migrations.ts`, which regenerates `src/server/db/migrations.gen.ts` (committed, AUTO-GENERATED header) by `import sql_<idx> from '#drizzle/<tag>.sql' with { type: 'text' }`. `src/cli/migrate.ts` reads `embeddedMigrations`, **validates SHA-256 hash of every already-applied migration against the embedded SQL** (rejects schema drift if anyone edited an applied migration), then applies pending entries via `db.execute(sql\`...\`)` + `db.transaction(...)` against the `drizzle.__drizzle_migrations` book-keeping table — public APIs only, no `db.dialect`/`db.session` (those are `@internal`). Each migration is split on `--> statement-breakpoint`; empty fragments are trimmed and skipped. Dev helpers `db:push` / `drizzle-kit migrate` still read `./drizzle/`.
|
||||
@@ -201,6 +202,7 @@ Nitro plugins are wired in `vite.config.ts` (`nitro({ plugins: [...] })`), not v
|
||||
|
||||
## Don'ts (specific, non-obvious)
|
||||
|
||||
- **Don't run `bun run db:generate` (or `drizzle-kit generate`) as an AI agent.** Migration generation is reserved for the human. Make schema changes in `src/server/db/schema/*` + `src/server/db/fields.ts`, push the code changes, and stop — the human will run `bun run db:generate` and commit the resulting `drizzle/*.sql` + `src/server/db/migrations.gen.ts` themselves. (`bun run db:embed` is also off-limits because it's the codegen tail of `db:generate`.)
|
||||
- Don't edit `routeTree.gen.ts` or `src/server/db/migrations.gen.ts`.
|
||||
- Don't eager-import anything from `.output/` in `src/bin.ts` or any module it statically imports — it starts the HTTP server as a side effect. Subcommands must be lazy via citty's `() => import(...)` thunks.
|
||||
- Don't re-add an auto-migrate Nitro plugin. Migrations are an explicit deploy step via `./server migrate`.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const generatedFields = {
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => Bun.randomUUIDv7()),
|
||||
id: uuid('id').primaryKey().default(sql`uuidv7()`),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||
.notNull()
|
||||
|
||||
Reference in New Issue
Block a user