docs(06-01): complete PostgreSQL store package plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
623cff0d76
commit
8237077728
1 changed files with 110 additions and 0 deletions
110
.planning/phases/06-lab-advisor/06-01-SUMMARY.md
Normal file
110
.planning/phases/06-lab-advisor/06-01-SUMMARY.md
Normal 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
|
||||
Loading…
Add table
Reference in a new issue