Files
GearBox/.planning/phases/14-postgresql-migration/14-01-PLAN.md

295 lines
13 KiB
Markdown

---
phase: 14-postgresql-migration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/db/schema.ts
- src/db/index.ts
- src/db/migrate.ts
- src/db/seed.ts
- src/shared/types.ts
- tests/helpers/db.ts
- drizzle.config.ts
- package.json
autonomous: true
requirements: [DB-01, DB-03]
must_haves:
truths:
- "Schema defines all 12 tables using drizzle-orm/pg-core (pgTable, serial, text, timestamp, etc.)"
- "Database connection uses postgres.js driver with DATABASE_URL"
- "Test helper creates async PGlite-backed Drizzle instance with migrations applied"
- "Drizzle migrations are generated in drizzle-pg/ directory"
artifacts:
- path: "src/db/schema.ts"
provides: "PostgreSQL table definitions"
contains: "pgTable"
- path: "src/db/index.ts"
provides: "Async Postgres connection"
contains: "drizzle-orm/postgres-js"
- path: "tests/helpers/db.ts"
provides: "PGlite test database factory"
contains: "drizzle-orm/pglite"
- path: "drizzle-pg/"
provides: "PostgreSQL migration files"
- path: "drizzle.config.ts"
provides: "Drizzle Kit config for PostgreSQL"
contains: "postgresql"
key_links:
- from: "tests/helpers/db.ts"
to: "src/db/schema.ts"
via: "import * as schema"
pattern: "import.*schema"
- from: "src/db/index.ts"
to: "src/db/schema.ts"
via: "import * as schema"
pattern: "import.*schema"
---
<objective>
Rewrite the database foundation from SQLite to PostgreSQL: schema definitions, database connection, test infrastructure, and Drizzle configuration. Install required packages. Generate the initial PostgreSQL migration.
Purpose: Everything else in this phase depends on these files. Schema and DB config must exist before services, routes, or tests can be converted.
Output: Working schema.ts (pg-core), index.ts (postgres.js), tests/helpers/db.ts (PGlite), drizzle.config.ts (postgresql), generated migration in drizzle-pg/
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-postgresql-migration/14-CONTEXT.md
@.planning/phases/14-postgresql-migration/14-RESEARCH.md
@src/db/schema.ts
@src/db/index.ts
@src/db/migrate.ts
@src/db/seed.ts
@src/shared/types.ts
@tests/helpers/db.ts
@drizzle.config.ts
@package.json
</context>
<tasks>
<task type="auto">
<name>Task 1: Install dependencies and rewrite schema + DB config files</name>
<files>package.json, src/db/schema.ts, src/db/index.ts, src/db/migrate.ts, src/db/seed.ts, src/shared/types.ts, drizzle.config.ts</files>
<read_first>src/db/schema.ts, src/db/index.ts, src/db/migrate.ts, src/db/seed.ts, src/shared/types.ts, drizzle.config.ts, package.json</read_first>
<action>
**Step 1: Install packages**
```bash
bun add postgres @electric-sql/pglite
bun remove better-sqlite3 @types/better-sqlite3
```
**Step 2: Rewrite `src/db/schema.ts`** -- Clean rewrite per D-01. Replace all `sqliteTable` with `pgTable`, all imports from `drizzle-orm/sqlite-core` with `drizzle-orm/pg-core`.
Column type mapping (apply to ALL 12 tables):
- `integer("id").primaryKey({ autoIncrement: true })` -> `serial("id").primaryKey()`
- `text("col")` -> `text("col")` (unchanged)
- `real("weight_grams")` -> `doublePrecision("weight_grams")`
- `real("sort_order")` -> `doublePrecision("sort_order")`
- `integer("price_cents")` -> `integer("price_cents")` (unchanged)
- `integer("col", { mode: "timestamp" }).$defaultFn(() => new Date())` -> `timestamp("col").notNull().defaultNow()`
- `integer("col", { mode: "timestamp" }).notNull()` (no default, e.g., expiresAt) -> `timestamp("col").notNull()`
- `integer("used").notNull().default(0)` -> `boolean("used").notNull().default(false)` (oauthCodes table)
- `integer("quantity").notNull().default(1)` -> `integer("quantity").notNull().default(1)` (unchanged)
Tables to rewrite (12 total): categories, items, threads, threadCandidates, setups, setupItems, settings, users, sessions, apiKeys, oauthClients, oauthCodes, oauthTokens.
Import statement:
```typescript
import { boolean, doublePrecision, integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
```
Preserve ALL foreign key references and cascade rules exactly as they are. Preserve all `.unique()` constraints. Preserve all `.default()` values.
For `settings` table: keep `text("key").primaryKey()` (no serial).
For `sessions` table: keep `text("id").primaryKey()` (no serial).
**Step 3: Rewrite `src/db/index.ts`** per D-03:
```typescript
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema.ts";
const connectionString = process.env.DATABASE_URL || "postgresql://gearbox:gearbox@localhost:5432/gearbox";
const queryClient = postgres(connectionString);
export const db = drizzle(queryClient, { schema });
```
**Step 4: Rewrite `src/db/migrate.ts`**:
```typescript
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL || "postgresql://gearbox:gearbox@localhost:5432/gearbox";
const migrationClient = postgres(connectionString, { max: 1 });
const db = drizzle(migrationClient);
await migrate(db, { migrationsFolder: "./drizzle-pg" });
await migrationClient.end();
console.log("Migrations applied successfully");
```
**Step 5: Rewrite `src/db/seed.ts`** to async:
```typescript
import { db } from "./index.ts";
import { categories } from "./schema.ts";
export async function seedDefaults() {
const existing = await db.select().from(categories);
if (existing.length === 0) {
await db.insert(categories).values({
name: "Uncategorized",
icon: "package",
});
}
}
```
**Step 6: Update `src/shared/types.ts`** -- No changes needed to the file content itself. The types infer from schema which still exports the same table names. Verify the file still compiles after schema change.
**Step 7: Update `drizzle.config.ts`** per D-02:
```typescript
import { defineConfig } from "drizzle-kit";
export default defineConfig({
out: "./drizzle-pg",
schema: "./src/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "postgresql://gearbox:gearbox@localhost:5432/gearbox",
},
});
```
</action>
<verify>
<automated>grep -q "pgTable" src/db/schema.ts && grep -q "drizzle-orm/pg-core" src/db/schema.ts && grep -q "postgres-js" src/db/index.ts && grep -q "postgresql" drizzle.config.ts && grep -q "async function seedDefaults" src/db/seed.ts && bun run lint 2>&1 | tail -3 && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- src/db/schema.ts contains `import { boolean, doublePrecision, integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"`
- src/db/schema.ts contains `pgTable("categories"` and all 12 table definitions use pgTable
- src/db/schema.ts does NOT contain `sqliteTable` or `drizzle-orm/sqlite-core` or `real(` or `{ mode: "timestamp" }`
- src/db/schema.ts contains `boolean("used")` for oauthCodes table
- src/db/schema.ts contains `doublePrecision("weight_grams")` and `doublePrecision("sort_order")`
- src/db/schema.ts contains `timestamp("created_at").notNull().defaultNow()` pattern
- src/db/index.ts contains `import postgres from "postgres"` and `drizzle-orm/postgres-js`
- src/db/index.ts contains `DATABASE_URL`
- src/db/index.ts does NOT contain `bun:sqlite`
- src/db/migrate.ts contains `drizzle-orm/postgres-js/migrator` and `migrationsFolder: "./drizzle-pg"`
- src/db/seed.ts contains `export async function seedDefaults()`
- src/db/seed.ts contains `await db.select()` and `await db.insert()`
- drizzle.config.ts contains `dialect: "postgresql"` and `out: "./drizzle-pg"`
- package.json contains `"postgres"` in dependencies
- package.json contains `"@electric-sql/pglite"` in devDependencies or dependencies
- package.json does NOT contain `"better-sqlite3"` or `"@types/better-sqlite3"`
</acceptance_criteria>
<done>All 12 tables rewritten with pg-core types. DB connection uses postgres.js. Migrate.ts uses postgres-js migrator. Seed is async. Drizzle config targets postgresql dialect with drizzle-pg/ output.</done>
</task>
<task type="auto">
<name>Task 2: Rewrite test helper and generate initial PostgreSQL migration</name>
<files>tests/helpers/db.ts, drizzle-pg/</files>
<read_first>tests/helpers/db.ts, src/db/schema.ts</read_first>
<action>
**Step 1: Rewrite `tests/helpers/db.ts`** per D-07 and D-08:
```typescript
import { drizzle } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
import * as schema from "../../src/db/schema.ts";
export async function createTestDb() {
const db = drizzle({ schema });
// Apply migrations from the new PostgreSQL migration directory
await migrate(db, { migrationsFolder: "./drizzle-pg" });
// Seed default Uncategorized category
await db.insert(schema.categories).values({ name: "Uncategorized", icon: "package" });
return db;
}
```
Key changes from current:
- Import from `drizzle-orm/pglite` instead of `drizzle-orm/bun-sqlite`
- `migrate` from `drizzle-orm/pglite/migrator` instead of `drizzle-orm/bun-sqlite/migrator`
- Function is now `async` (returns Promise)
- No `Database` import from `bun:sqlite`
- No `":memory:"` -- PGlite creates an in-memory Postgres instance by default
- Migration folder changed to `./drizzle-pg`
- `db.insert(...).values(...).run()` becomes `await db.insert(...).values(...)`
**Step 2: Generate initial PostgreSQL migration:**
```bash
bunx drizzle-kit generate
```
This reads the updated `drizzle.config.ts` (dialect: "postgresql", schema: src/db/schema.ts) and generates SQL migration files in `drizzle-pg/`.
**Step 3: Verify migration was generated and is complete:**
```bash
ls drizzle-pg/
cat drizzle-pg/*.sql
```
Confirm the SQL contains `CREATE TABLE` statements for all 12 tables with correct Postgres types (serial, text, timestamp, boolean, double precision, etc.). Count the CREATE TABLE statements -- there must be at least 12 (categories, items, threads, thread_candidates, setups, setup_items, settings, users, sessions, api_keys, oauth_clients, oauth_codes, oauth_tokens).
**Step 4: Quick smoke test -- verify PGlite test helper works:**
```bash
bun -e "
import { createTestDb } from './tests/helpers/db.ts';
const db = await createTestDb();
const cats = await db.select().from((await import('./src/db/schema.ts')).categories);
console.log('Categories:', cats.length);
if (cats.length !== 1) { console.error('FAIL: expected 1 category'); process.exit(1); }
console.log('PGlite test helper works!');
"
```
</action>
<verify>
<automated>ls drizzle-pg/*.sql && grep -c "CREATE TABLE" drizzle-pg/*.sql | tail -1 | grep -qE "^drizzle-pg/.*:1[2-9]$|^drizzle-pg/.*:[2-9][0-9]$" || { echo "WARNING: verify CREATE TABLE count manually"; }; grep -q "drizzle-orm/pglite" tests/helpers/db.ts && grep -q "async function createTestDb" tests/helpers/db.ts && bun run lint 2>&1 | tail -3 && echo "PASS" || echo "FAIL"</automated>
</verify>
<acceptance_criteria>
- tests/helpers/db.ts contains `import { drizzle } from "drizzle-orm/pglite"`
- tests/helpers/db.ts contains `import { migrate } from "drizzle-orm/pglite/migrator"`
- tests/helpers/db.ts contains `export async function createTestDb()`
- tests/helpers/db.ts contains `migrationsFolder: "./drizzle-pg"`
- tests/helpers/db.ts does NOT contain `bun:sqlite` or `drizzle-orm/bun-sqlite` or `.run()`
- drizzle-pg/ directory exists with at least one .sql migration file
- Migration SQL contains CREATE TABLE for all 12+ tables (categories, items, threads, thread_candidates, setups, setup_items, settings, users, sessions, api_keys, oauth_clients, oauth_codes, oauth_tokens)
- `grep -c "CREATE TABLE" drizzle-pg/*.sql` shows at least 12 CREATE TABLE statements
- PGlite smoke test (bun -e script above) exits 0
</acceptance_criteria>
<done>Test helper returns async PGlite Drizzle instance. Initial PostgreSQL migration generated in drizzle-pg/ with all 12+ CREATE TABLE statements. Smoke test confirms PGlite can apply migrations and seed data.</done>
</task>
</tasks>
<verification>
- `grep -r "sqliteTable\|bun:sqlite\|drizzle-orm/sqlite-core\|drizzle-orm/bun-sqlite" src/db/ drizzle.config.ts tests/helpers/db.ts` returns NO matches
- `grep -c "pgTable" src/db/schema.ts` returns 12+ (one per table, possibly more from import)
- `ls drizzle-pg/*.sql` shows at least one migration file
- `grep -c "CREATE TABLE" drizzle-pg/*.sql` shows at least 12 tables
- PGlite smoke test exits 0
- `bun run lint` passes
</verification>
<success_criteria>
All database foundation files rewritten for PostgreSQL. Schema uses pg-core types. DB connection uses postgres.js. Test helper uses PGlite. Initial migration generated with all 12+ tables. No SQLite references remain in these files. Lint passes.
</success_criteria>
<output>
After completion, create `.planning/phases/14-postgresql-migration/14-01-SUMMARY.md`
</output>