docs(06-01): complete PostgreSQL store package plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikkel Georgsen 2026-04-10 07:31:59 +00:00
parent 623cff0d76
commit 8237077728

View file

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