litclaude-ai 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/LICENSE +21 -0
- package/README.md +369 -0
- package/README_ko-KR.md +374 -0
- package/RELEASE_CHECKLIST.md +165 -0
- package/bin/litclaude-ai.js +643 -0
- package/cover.png +0 -0
- package/docs/agents.md +67 -0
- package/docs/hooks.md +134 -0
- package/docs/lsp.md +40 -0
- package/docs/migration.md +209 -0
- package/docs/workflow-compatibility-audit.md +119 -0
- package/generate_cover.py +123 -0
- package/package.json +48 -0
- package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
- package/plugins/litclaude/.lsp.json +13 -0
- package/plugins/litclaude/.mcp.json +9 -0
- package/plugins/litclaude/agents/boulder-executor.md +12 -0
- package/plugins/litclaude/agents/librarian-researcher.md +15 -0
- package/plugins/litclaude/agents/oracle-verifier.md +16 -0
- package/plugins/litclaude/agents/prometheus-planner.md +13 -0
- package/plugins/litclaude/agents/qa-runner.md +16 -0
- package/plugins/litclaude/agents/quality-reviewer.md +17 -0
- package/plugins/litclaude/bin/litclaude-hook.js +110 -0
- package/plugins/litclaude/bin/litclaude-hud.js +271 -0
- package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
- package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
- package/plugins/litclaude/commands/deep-interview.md +21 -0
- package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
- package/plugins/litclaude/commands/lit-loop.md +40 -0
- package/plugins/litclaude/commands/lit-plan.md +35 -0
- package/plugins/litclaude/commands/litgoal.md +30 -0
- package/plugins/litclaude/commands/review-work.md +35 -0
- package/plugins/litclaude/commands/start-work.md +36 -0
- package/plugins/litclaude/hooks/hooks.json +54 -0
- package/plugins/litclaude/lib/context-pressure.mjs +25 -0
- package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
- package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
- package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
- package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
- package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
- package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
- package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
- package/plugins/litclaude/lib/workflow-check.mjs +83 -0
- package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
- package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
- package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
- package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
- package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
- package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
- package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
- package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
- package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
- package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
- package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
- package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
- package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
- package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
- package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
- package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
- package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
- package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
- package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
- package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
- package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
- package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
- package/plugins/litclaude/skills/programming/SKILL.md +106 -0
- package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
- package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
- package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
- package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
- package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
- package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
- package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
- package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
- package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
- package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
- package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
- package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
- package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
- package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
- package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
- package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
- package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
- package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
- package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
- package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
- package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
- package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
- package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
- package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
- package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
- package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
- package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
- package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
- package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
- package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
- package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
- package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
- package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
- package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
- package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
- package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
- package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
- package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
- package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
- package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
- package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
- package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
- package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
- package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
- package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
- package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
- package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
- package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
- package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
- package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
- package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
- package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
- package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
- package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
- package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
- package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
- package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
- package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
- package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
- package/plugins/litclaude/skills/rules/SKILL.md +66 -0
- package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
- package/scripts/audit-plan-checkboxes.mjs +37 -0
- package/scripts/doctor.mjs +41 -0
- package/scripts/inspect-agent-tools.mjs +27 -0
- package/scripts/postinstall.mjs +50 -0
- package/scripts/qa-claude-plugin-smoke.sh +60 -0
- package/scripts/qa-portable-install.sh +136 -0
- package/scripts/validate-plugin.mjs +72 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Database Stack — sqlc + pgx + goose + testcontainers
|
|
2
|
+
|
|
3
|
+
The canonical 2026 PostgreSQL stack. **Type-safe SQL with zero runtime reflection**, hot-path-friendly connection pooling, sane migrations, real Postgres in tests.
|
|
4
|
+
|
|
5
|
+
If you came here from a `gorm` project: gorm is rejected. See "Why not gorm" at the end.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Toolchain
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
|
13
|
+
go install github.com/pressly/goose/v3/cmd/goose@latest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
internal/store/
|
|
22
|
+
├── sqlc.yaml # sqlc config
|
|
23
|
+
├── schema.sql # the cumulative DDL sqlc parses
|
|
24
|
+
├── queries/ # one *.sql per resource
|
|
25
|
+
│ ├── users.sql
|
|
26
|
+
│ ├── orders.sql
|
|
27
|
+
│ └── sessions.sql
|
|
28
|
+
├── sqlc/ # GENERATED — do not hand-edit
|
|
29
|
+
│ ├── db.go
|
|
30
|
+
│ ├── models.go
|
|
31
|
+
│ ├── users.sql.go
|
|
32
|
+
│ ├── orders.sql.go
|
|
33
|
+
│ └── sessions.sql.go
|
|
34
|
+
├── migrations/ # goose migrations, ordered
|
|
35
|
+
│ ├── 20260101000001_create_users.sql
|
|
36
|
+
│ └── 20260102000001_add_orders.sql
|
|
37
|
+
├── pool.go # pgxpool factory
|
|
38
|
+
├── user_store.go # domain-facing wrapper around sqlc
|
|
39
|
+
└── user_store_test.go # testcontainers integration test
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## `sqlc.yaml`
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
version: "2"
|
|
48
|
+
sql:
|
|
49
|
+
- engine: "postgresql"
|
|
50
|
+
schema: "schema.sql"
|
|
51
|
+
queries: "queries"
|
|
52
|
+
gen:
|
|
53
|
+
go:
|
|
54
|
+
package: "sqlc"
|
|
55
|
+
out: "sqlc"
|
|
56
|
+
sql_package: "pgx/v5"
|
|
57
|
+
emit_json_tags: false
|
|
58
|
+
emit_prepared_queries: false
|
|
59
|
+
emit_interface: true # generates a Querier interface
|
|
60
|
+
emit_exact_table_names: false
|
|
61
|
+
emit_pointers_for_null_types: true
|
|
62
|
+
emit_empty_slices: true
|
|
63
|
+
overrides:
|
|
64
|
+
- db_type: "uuid"
|
|
65
|
+
go_type:
|
|
66
|
+
import: "github.com/google/uuid"
|
|
67
|
+
type: "UUID"
|
|
68
|
+
- db_type: "timestamptz"
|
|
69
|
+
go_type:
|
|
70
|
+
import: "time"
|
|
71
|
+
type: "Time"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Key choices:
|
|
75
|
+
|
|
76
|
+
- `sql_package: "pgx/v5"` — generated code uses pgx directly, not `database/sql`. Faster, type-safer.
|
|
77
|
+
- `emit_interface: true` — generates a `Querier` interface. Lets stores accept either `*pgxpool.Pool` or `pgx.Tx` for transaction support.
|
|
78
|
+
- `emit_pointers_for_null_types: true` — nullable columns become `*T`, not `sql.NullString`. Cleaner mapping to domain types.
|
|
79
|
+
- `overrides` for `uuid` → `google/uuid.UUID` and `timestamptz` → `time.Time`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## `schema.sql`
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
-- internal/store/schema.sql
|
|
87
|
+
-- The CUMULATIVE schema sqlc parses. Not migrations — the end state.
|
|
88
|
+
-- Regenerate from a fresh DB via `pg_dump --schema-only`, or hand-maintain.
|
|
89
|
+
|
|
90
|
+
CREATE TABLE users (
|
|
91
|
+
id UUID PRIMARY KEY,
|
|
92
|
+
email TEXT NOT NULL UNIQUE,
|
|
93
|
+
username TEXT NOT NULL UNIQUE,
|
|
94
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE INDEX idx_users_created_at ON users(created_at DESC);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## `queries/users.sql`
|
|
103
|
+
|
|
104
|
+
```sql
|
|
105
|
+
-- name: GetUser :one
|
|
106
|
+
SELECT id, email, username, created_at
|
|
107
|
+
FROM users
|
|
108
|
+
WHERE id = $1;
|
|
109
|
+
|
|
110
|
+
-- name: ListUsers :many
|
|
111
|
+
SELECT id, email, username, created_at
|
|
112
|
+
FROM users
|
|
113
|
+
ORDER BY created_at DESC
|
|
114
|
+
LIMIT $1 OFFSET $2;
|
|
115
|
+
|
|
116
|
+
-- name: CreateUser :one
|
|
117
|
+
INSERT INTO users (id, email, username)
|
|
118
|
+
VALUES ($1, $2, $3)
|
|
119
|
+
RETURNING id, email, username, created_at;
|
|
120
|
+
|
|
121
|
+
-- name: UpdateUserEmail :exec
|
|
122
|
+
UPDATE users
|
|
123
|
+
SET email = $2
|
|
124
|
+
WHERE id = $1;
|
|
125
|
+
|
|
126
|
+
-- name: DeleteUser :exec
|
|
127
|
+
DELETE FROM users WHERE id = $1;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
sqlc directives:
|
|
131
|
+
|
|
132
|
+
- `:one` — exactly one row; returns `(T, error)`. Returns `pgx.ErrNoRows` on miss.
|
|
133
|
+
- `:many` — zero or more rows; returns `([]T, error)`.
|
|
134
|
+
- `:exec` — no rows returned; returns `error`.
|
|
135
|
+
- `:execrows` — returns `(int64, error)` with affected row count.
|
|
136
|
+
- `:batchone` / `:batchmany` / `:batchexec` — pgx batch mode for bulk operations.
|
|
137
|
+
|
|
138
|
+
Run `task gen:sqlc` (or `sqlc generate`). The generated file is committed; CI checks it is up-to-date.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Generated code shape (`sqlc/users.sql.go`)
|
|
143
|
+
|
|
144
|
+
```go
|
|
145
|
+
// GENERATED — do not edit
|
|
146
|
+
type User struct {
|
|
147
|
+
ID uuid.UUID
|
|
148
|
+
Email string
|
|
149
|
+
Username string
|
|
150
|
+
CreatedAt time.Time
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const getUser = `-- name: GetUser :one
|
|
154
|
+
SELECT id, email, username, created_at FROM users WHERE id = $1`
|
|
155
|
+
|
|
156
|
+
func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) (User, error) {
|
|
157
|
+
row := q.db.QueryRow(ctx, getUser, id)
|
|
158
|
+
var u User
|
|
159
|
+
err := row.Scan(&u.ID, &u.Email, &u.Username, &u.CreatedAt)
|
|
160
|
+
return u, err
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Type-safe inputs, type-safe outputs, compile-time-checked column-to-field mapping. **A schema change that drops a column breaks compilation.** Hand-rolled SQL would have failed at runtime.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## `store/pool.go`
|
|
169
|
+
|
|
170
|
+
```go
|
|
171
|
+
package store
|
|
172
|
+
|
|
173
|
+
import (
|
|
174
|
+
"context"
|
|
175
|
+
"fmt"
|
|
176
|
+
"time"
|
|
177
|
+
|
|
178
|
+
"github.com/jackc/pgx/v5/pgxpool"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
|
|
182
|
+
cfg, err := pgxpool.ParseConfig(dsn)
|
|
183
|
+
if err != nil { return nil, fmt.Errorf("parse dsn: %w", err) }
|
|
184
|
+
|
|
185
|
+
cfg.MaxConns = 25
|
|
186
|
+
cfg.MinConns = 5
|
|
187
|
+
cfg.MaxConnLifetime = time.Hour
|
|
188
|
+
cfg.MaxConnIdleTime = 30 * time.Minute
|
|
189
|
+
cfg.HealthCheckPeriod = 1 * time.Minute
|
|
190
|
+
|
|
191
|
+
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
|
192
|
+
if err != nil { return nil, fmt.Errorf("connect: %w", err) }
|
|
193
|
+
|
|
194
|
+
if err := pool.Ping(ctx); err != nil {
|
|
195
|
+
pool.Close()
|
|
196
|
+
return nil, fmt.Errorf("ping: %w", err)
|
|
197
|
+
}
|
|
198
|
+
return pool, nil
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`pgxpool.Pool` is `Querier`-compatible (implements the interface sqlc generates). Same pool flows into sqlc queries unchanged.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## `store/user_store.go` — domain ↔ sqlc
|
|
207
|
+
|
|
208
|
+
```go
|
|
209
|
+
package store
|
|
210
|
+
|
|
211
|
+
import (
|
|
212
|
+
"context"
|
|
213
|
+
"errors"
|
|
214
|
+
"fmt"
|
|
215
|
+
|
|
216
|
+
"github.com/google/uuid"
|
|
217
|
+
"github.com/jackc/pgx/v5"
|
|
218
|
+
"github.com/jackc/pgx/v5/pgxpool"
|
|
219
|
+
|
|
220
|
+
"github.com/your-org/myservice/internal/domain"
|
|
221
|
+
"github.com/your-org/myservice/internal/store/sqlc"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
type UserStore struct {
|
|
225
|
+
q *sqlc.Queries
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func NewUserStore(pool *pgxpool.Pool) *UserStore {
|
|
229
|
+
return &UserStore{q: sqlc.New(pool)}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func (s *UserStore) Get(ctx context.Context, id domain.UserID) (domain.User, error) {
|
|
233
|
+
row, err := s.q.GetUser(ctx, uuid.UUID(id))
|
|
234
|
+
if err != nil {
|
|
235
|
+
if errors.Is(err, pgx.ErrNoRows) {
|
|
236
|
+
return domain.User{}, domain.ErrUserNotFound
|
|
237
|
+
}
|
|
238
|
+
return domain.User{}, fmt.Errorf("get user %s: %w", id, err)
|
|
239
|
+
}
|
|
240
|
+
return rowToDomain(row)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func (s *UserStore) Create(ctx context.Context, u domain.User) (domain.User, error) {
|
|
244
|
+
row, err := s.q.CreateUser(ctx, sqlc.CreateUserParams{
|
|
245
|
+
ID: uuid.UUID(u.ID),
|
|
246
|
+
Email: u.Email.String(),
|
|
247
|
+
Username: u.Username.String(),
|
|
248
|
+
})
|
|
249
|
+
if err != nil {
|
|
250
|
+
return domain.User{}, fmt.Errorf("create user: %w", err)
|
|
251
|
+
}
|
|
252
|
+
return rowToDomain(row)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func rowToDomain(r sqlc.User) (domain.User, error) {
|
|
256
|
+
email, err := domain.NewEmail(r.Email)
|
|
257
|
+
if err != nil {
|
|
258
|
+
return domain.User{}, fmt.Errorf("db invariant: email %q: %w", r.Email, err)
|
|
259
|
+
}
|
|
260
|
+
username, err := domain.NewUsername(r.Username)
|
|
261
|
+
if err != nil {
|
|
262
|
+
return domain.User{}, fmt.Errorf("db invariant: username %q: %w", r.Username, err)
|
|
263
|
+
}
|
|
264
|
+
return domain.User{
|
|
265
|
+
ID: domain.UserID(r.ID),
|
|
266
|
+
Email: email,
|
|
267
|
+
Username: username,
|
|
268
|
+
CreatedAt: r.CreatedAt,
|
|
269
|
+
}, nil
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The wrapping is verbose. **That is the point.** sqlc rows are storage representations; domain types are business representations. Mapping them explicitly is where invariants are enforced.
|
|
274
|
+
|
|
275
|
+
`pgx.ErrNoRows` becomes `domain.ErrUserNotFound` — callers never see storage-level errors.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Transactions — pgx.Tx satisfies the Querier interface
|
|
280
|
+
|
|
281
|
+
```go
|
|
282
|
+
func (s *UserStore) CreateWithProfile(
|
|
283
|
+
ctx context.Context,
|
|
284
|
+
pool *pgxpool.Pool,
|
|
285
|
+
u domain.User,
|
|
286
|
+
p domain.Profile,
|
|
287
|
+
) error {
|
|
288
|
+
tx, err := pool.Begin(ctx)
|
|
289
|
+
if err != nil { return fmt.Errorf("begin: %w", err) }
|
|
290
|
+
defer tx.Rollback(ctx) // no-op if Commit succeeded
|
|
291
|
+
|
|
292
|
+
q := s.q.WithTx(tx) // sqlc.Queries bound to the tx
|
|
293
|
+
|
|
294
|
+
if _, err := q.CreateUser(ctx, /* ... */); err != nil {
|
|
295
|
+
return fmt.Errorf("create user: %w", err)
|
|
296
|
+
}
|
|
297
|
+
if _, err := q.CreateProfile(ctx, /* ... */); err != nil {
|
|
298
|
+
return fmt.Errorf("create profile: %w", err)
|
|
299
|
+
}
|
|
300
|
+
return tx.Commit(ctx)
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Pattern:
|
|
305
|
+
|
|
306
|
+
- `defer tx.Rollback(ctx)` immediately after `Begin` — safe even after Commit (returns "tx closed", which we ignore via the unhandled return).
|
|
307
|
+
- `q.WithTx(tx)` returns a `*Queries` bound to the tx.
|
|
308
|
+
- Last line: `tx.Commit(ctx)`.
|
|
309
|
+
|
|
310
|
+
For nested transactions across multiple stores, accept a `Querier` parameter:
|
|
311
|
+
|
|
312
|
+
```go
|
|
313
|
+
func (s *UserStore) CreateTx(ctx context.Context, q sqlc.Querier, u domain.User) (domain.User, error) {
|
|
314
|
+
// uses q instead of s.q — caller decides if it's pool or tx
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Migrations — goose
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
goose -dir internal/store/migrations create create_users sql
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```sql
|
|
327
|
+
-- migrations/20260101000001_create_users.sql
|
|
328
|
+
-- +goose Up
|
|
329
|
+
CREATE TABLE users (
|
|
330
|
+
id UUID PRIMARY KEY,
|
|
331
|
+
email TEXT NOT NULL UNIQUE,
|
|
332
|
+
username TEXT NOT NULL UNIQUE,
|
|
333
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
-- +goose Down
|
|
337
|
+
DROP TABLE users;
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Run:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
goose -dir internal/store/migrations postgres "$DATABASE_URL" up
|
|
344
|
+
goose -dir internal/store/migrations postgres "$DATABASE_URL" status
|
|
345
|
+
goose -dir internal/store/migrations postgres "$DATABASE_URL" down
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Rules:
|
|
349
|
+
|
|
350
|
+
- One DDL change per migration. Never combine schema + data migrations in one file.
|
|
351
|
+
- `Down` is real, not a stub. CI runs `up` → `down` → `up` on a fresh container to prove reversibility.
|
|
352
|
+
- Migrations are append-only. Never edit a merged migration; add a new one.
|
|
353
|
+
|
|
354
|
+
`goose` can run programmatically as well:
|
|
355
|
+
|
|
356
|
+
```go
|
|
357
|
+
import "github.com/pressly/goose/v3"
|
|
358
|
+
|
|
359
|
+
if err := goose.UpContext(ctx, db, "migrations"); err != nil { ... }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Useful for tools that own their schema (CI runner, integration test setup).
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Integration tests — testcontainers
|
|
367
|
+
|
|
368
|
+
```go
|
|
369
|
+
package store_test
|
|
370
|
+
|
|
371
|
+
import (
|
|
372
|
+
"context"
|
|
373
|
+
"testing"
|
|
374
|
+
|
|
375
|
+
"github.com/stretchr/testify/require"
|
|
376
|
+
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
func newTestDB(t *testing.T) *pgxpool.Pool {
|
|
380
|
+
t.Helper()
|
|
381
|
+
ctx := context.Background()
|
|
382
|
+
|
|
383
|
+
pgC, err := postgres.Run(ctx,
|
|
384
|
+
"postgres:16-alpine",
|
|
385
|
+
postgres.WithDatabase("test"),
|
|
386
|
+
postgres.WithUsername("test"),
|
|
387
|
+
postgres.WithPassword("test"),
|
|
388
|
+
postgres.BasicWaitStrategies(),
|
|
389
|
+
)
|
|
390
|
+
require.NoError(t, err)
|
|
391
|
+
t.Cleanup(func() { _ = pgC.Terminate(ctx) })
|
|
392
|
+
|
|
393
|
+
dsn, err := pgC.ConnectionString(ctx, "sslmode=disable")
|
|
394
|
+
require.NoError(t, err)
|
|
395
|
+
|
|
396
|
+
pool, err := store.NewPool(ctx, dsn)
|
|
397
|
+
require.NoError(t, err)
|
|
398
|
+
t.Cleanup(pool.Close)
|
|
399
|
+
|
|
400
|
+
require.NoError(t, goose.UpContext(ctx, /* sql.DB from pool */, "../migrations"))
|
|
401
|
+
return pool
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
func TestUserStore_Create_returns_new_user(t *testing.T) {
|
|
405
|
+
// Given
|
|
406
|
+
pool := newTestDB(t)
|
|
407
|
+
s := store.NewUserStore(pool)
|
|
408
|
+
ctx := context.Background()
|
|
409
|
+
|
|
410
|
+
// When
|
|
411
|
+
user, err := s.Create(ctx, domain.User{
|
|
412
|
+
ID: domain.UserID(uuid.Must(uuid.NewV7())),
|
|
413
|
+
Email: mustEmail("a@b.com"),
|
|
414
|
+
Username: mustUsername("alice"),
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// Then
|
|
418
|
+
require.NoError(t, err)
|
|
419
|
+
require.NotEmpty(t, user.ID)
|
|
420
|
+
|
|
421
|
+
fetched, err := s.Get(ctx, user.ID)
|
|
422
|
+
require.NoError(t, err)
|
|
423
|
+
require.Equal(t, user.Email, fetched.Email)
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
testcontainers spins a real Postgres in Docker, runs migrations, hands you a pool. Tests are slow (~2s startup) but **real** — no fake that diverges from production.
|
|
428
|
+
|
|
429
|
+
For test suites with many cases, share one container across tests in the same package via `TestMain`:
|
|
430
|
+
|
|
431
|
+
```go
|
|
432
|
+
var testPool *pgxpool.Pool
|
|
433
|
+
|
|
434
|
+
func TestMain(m *testing.M) {
|
|
435
|
+
ctx := context.Background()
|
|
436
|
+
pgC, _ := postgres.Run(ctx, "postgres:16-alpine", /* ... */)
|
|
437
|
+
defer pgC.Terminate(ctx)
|
|
438
|
+
dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable")
|
|
439
|
+
testPool, _ = store.NewPool(ctx, dsn)
|
|
440
|
+
// run migrations once
|
|
441
|
+
os.Exit(m.Run())
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Each test then uses a transaction it rolls back at the end — fast and isolated.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Why NOT gorm
|
|
450
|
+
|
|
451
|
+
| Concern | gorm | sqlc + pgx |
|
|
452
|
+
|---|---|---|
|
|
453
|
+
| Type safety | runtime reflection; column-to-field via tags | compile-time-checked from SQL |
|
|
454
|
+
| Performance | 2–5x slower than pgx | pgx is the fastest Go pg driver |
|
|
455
|
+
| N+1 queries | encouraged by `Preload` API | explicit JOIN in `.sql` |
|
|
456
|
+
| Migrations | AutoMigrate (unsafe in prod) | goose, explicit |
|
|
457
|
+
| Debugging | "what query did it run?" requires logging | the query IS the source |
|
|
458
|
+
| Cancellation | spotty ctx support | first-class |
|
|
459
|
+
| Active development | Yes but with churn and breaking changes | sqlc is stable |
|
|
460
|
+
|
|
461
|
+
Existing gorm projects: leave them. New code: sqlc + pgx.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Sources
|
|
466
|
+
|
|
467
|
+
- sqlc docs: https://docs.sqlc.dev
|
|
468
|
+
- pgx: https://github.com/jackc/pgx
|
|
469
|
+
- goose: https://github.com/pressly/goose
|
|
470
|
+
- testcontainers-go: https://golang.testcontainers.org
|
|
471
|
+
- pgx pool config: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config
|