diff --git a/.planning/phases/06-lab-advisor/06-01-SUMMARY.md b/.planning/phases/06-lab-advisor/06-01-SUMMARY.md new file mode 100644 index 0000000..4a5d1d6 --- /dev/null +++ b/.planning/phases/06-lab-advisor/06-01-SUMMARY.md @@ -0,0 +1,110 @@ +--- +phase: 06-lab-advisor +plan: "01" +subsystem: store +tags: [postgresql, pgx, persistence, store, migrations] + +dependency_graph: + requires: [] + provides: [internal/store] + affects: [06-02, 06-03] + +tech_stack: + added: + - github.com/jackc/pgx/v5 v5.9.1 + - github.com/jackc/puddle/v2 v2.2.2 + - github.com/jackc/pgpassfile v1.0.0 + - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 + patterns: + - pgxpool for connection pooling (not single conn) + - Parameterized $N queries throughout (no string interpolation) + - ErrNotFound sentinel for missing resources + - Integration tests gated behind `//go:build integration` tag + +key_files: + created: + - internal/store/store.go + - internal/store/migrations.go + - internal/store/conversations.go + - internal/store/store_test.go + modified: + - go.mod + - go.sum + +decisions: + - Used dot-import in external test package to avoid verbose `store.NewStore` prefix while keeping package boundary clean + - Used LEFT JOIN in GetConversation to handle conversations with zero messages, scanning nullable message columns + - Added LIMIT 100 to ListConversations per T-06-01-04 (DoS guard for unbounded growth) + - DSN never logged on connection error — only error text forwarded (T-06-01-02) + +metrics: + duration_seconds: 124 + completed_at: "2026-04-10T07:31:15Z" + tasks_completed: 2 + tasks_total: 2 + files_created: 4 + files_modified: 2 +--- + +# Phase 06 Plan 01: PostgreSQL Store Package Summary + +**One-liner:** pgx/v5 connection pool with idempotent schema migrations and typed CRUD for conversations + messages, backed by live PostgreSQL at 10.5.0.109. + +## Tasks Completed + +| # | Name | Commit | Files | +|---|------|--------|-------| +| 1 | Add pgx/v5 dep and create Store with RunMigrations | 4bc22dc | store.go, migrations.go, go.mod, go.sum | +| 2 | Conversation and message CRUD methods | 623cff0 | conversations.go, store_test.go | + +## What Was Built + +### internal/store/store.go +`Store` struct wrapping `*pgxpool.Pool`. `NewStore(ctx, dsn)` opens the pool and pings before returning. `Close()` drains the pool. `Pool()` accessor gives migrations and tests direct pool access. + +### internal/store/migrations.go +`RunMigrations(ctx, pool)` executes two `CREATE TABLE IF NOT EXISTS` statements in dependency order (conversations first, then messages which references it via FK). Safe to call on every startup. + +Schema: +- `conversations(id UUID PK DEFAULT gen_random_uuid(), started_at TIMESTAMPTZ DEFAULT now(), model TEXT DEFAULT '')` +- `messages(id UUID PK, conversation_id UUID FK REFERENCES conversations ON DELETE CASCADE, role TEXT CHECK IN ('user','assistant','system'), content TEXT, created_at TIMESTAMPTZ DEFAULT now())` + +### internal/store/conversations.go +Types: `Message`, `Conversation`, `ConversationSummary`, `ErrNotFound`. + +Methods on `*Store`: +- `CreateConversation(ctx, model) (id string, err error)` +- `AddMessage(ctx, conversationID, role, content) (id string, err error)` — DB CHECK constraint rejects invalid roles +- `GetConversation(ctx, id) (*Conversation, error)` — LEFT JOIN to handle zero-message conversations; returns `ErrNotFound` on miss +- `ListConversations(ctx) ([]ConversationSummary, error)` — COUNT(m.id) aggregation with LIMIT 100 soft guard + +### internal/store/store_test.go +Integration tests (build tag: `integration`) covering all behaviors from the plan spec. Each sub-test creates its own fixtures and cleans up via deferred DELETE. Tests connect to the real PostgreSQL instance. + +## Deviations from Plan + +None — plan executed exactly as written. The dot-import (`import . "git.georgsen.dk/hwlab/internal/store"`) in the test file is a minor stylistic choice for readability in external test packages, consistent with common Go testing practice for the same package's integration tests. + +## Threat Mitigations Applied + +| Threat ID | Mitigation | +|-----------|-----------| +| T-06-01-01 | All queries use `$1`/`$2` parameterized args via pgx; zero string interpolation in SQL | +| T-06-01-02 | `NewStore` wraps error without including DSN; only pgx error text is forwarded | +| T-06-01-03 | DB-level `CHECK (role IN ('user','assistant','system'))` — verified by test | +| T-06-01-04 | `LIMIT 100` added to `ListConversations` | + +## Known Stubs + +None — all methods are fully wired to the live database. + +## Self-Check: PASSED + +- internal/store/store.go: exists +- internal/store/migrations.go: exists +- internal/store/conversations.go: exists +- internal/store/store_test.go: exists +- Commit 4bc22dc: verified in git log +- Commit 623cff0: verified in git log +- `go build ./...`: PASSED +- Integration tests: 12/12 PASSED