Files
Vector/packages/db/POSTGRES_FTS.md
T
josh 7c0d422228
CI / Lint · Typecheck · Test · Build (push) Failing after 5m41s
CI / Playwright (smoke) (push) Has been skipped
chore: initial Vector 2.0 monorepo
Ground-up TypeScript rewrite of the Vector hardware parts inventory
system. Ships the full roadmap (Phases 0-8) in one initial commit:

- pnpm + Turbo monorepo: apps/{api,web,e2e}, packages/{db,shared,ui,config}
- Express 5 + Prisma 5 + zod validation + JWT w/ refresh-token rotation
- React 19 + Vite + shadcn/ui + TanStack Query/Table + nuqs URL state
- Repair/RMA, tags, bulk ops, saved views, CSV audit export
- Analytics dashboard on Recharts + EOL tracking
- Signed webhook subscriptions (HMAC-SHA256) with in-process emitter
- Vitest unit tests (shared schemas, api services/helpers) + Playwright skeleton
- Gitea Actions CI (lint, typecheck, test+coverage, build) + Renovate

Deferred follow-ups: Postgres cutover (data-migration script ready),
BullMQ worker for webhook delivery, @react-pdf PDF export, CSV import wizard.
2026-04-16 20:52:32 -04:00

79 lines
2.3 KiB
Markdown

# Postgres-only migrations (apply post-cutover)
Phase 3 locked in the full schema shape on SQLite. When the datasource flips to
`postgresql` we apply a follow-up migration that upgrades a few columns to
Postgres-native types and adds the Full-Text Search column required by the plan.
## 1. Part full-text search (tsvector + GIN)
```sql
ALTER TABLE "Part"
ADD COLUMN "searchVector" tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('simple', coalesce("serialNumber", '')), 'A') ||
setweight(to_tsvector('simple', coalesce("mpn", '')), 'B') ||
setweight(to_tsvector('english', coalesce("notes", '')), 'C')
) STORED;
CREATE INDEX "Part_searchVector_idx" ON "Part" USING GIN ("searchVector");
```
Query shape the API will use for the `q=` filter on `/api/parts`:
```sql
SELECT * FROM "Part"
WHERE "searchVector" @@ plainto_tsquery('simple', $1)
ORDER BY ts_rank("searchVector", plainto_tsquery('simple', $1)) DESC
LIMIT $2 OFFSET $3;
```
Expose on the Prisma model with `@ignore` (Prisma can't represent `GENERATED`
columns) and read via `prisma.$queryRaw` inside the parts service.
## 2. Convert JSON-string columns to native Postgres types
These were stored as `String` for SQLite portability.
```sql
ALTER TABLE "WebhookSubscription"
ALTER COLUMN "events" TYPE text[] USING string_to_array(
trim(both '[]' from "events"), ','
);
ALTER TABLE "SavedView"
ALTER COLUMN "filterJson" TYPE jsonb USING "filterJson"::jsonb;
ALTER TABLE "CsvImportJob"
ALTER COLUMN "errors" TYPE jsonb USING "errors"::jsonb;
```
After the DDL change, update `packages/db/prisma/schema.prisma`:
```prisma
model WebhookSubscription {
// ...
events String[]
}
model SavedView {
// ...
filterJson Json
}
model CsvImportJob {
// ...
errors Json?
}
```
Regenerate the Prisma client and tighten the zod → Prisma marshaling in the
service layer (drop the `JSON.stringify` / `JSON.parse` bridges).
## Verification checklist (P3)
- `prisma migrate deploy` applies cleanly against a fresh Postgres snapshot.
- `EXPLAIN ANALYZE` on the FTS query shows a `Bitmap Index Scan` on
`Part_searchVector_idx` and returns ranked results in <50ms at 100k rows.
- Integration test inserts 3 parts with overlapping serial/mpn tokens, queries
`q=...`, and asserts order-by-rank.