295 lines
13 KiB
Markdown
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>
|