--- 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