PostgreSQL
Edit on GitHubReference for the go-chi-postgres deployment target
Last updated:
This page documents what the compiler emits when a spec is compiled against the go-chi-postgres
target. It is the concrete companion to the target-agnostic
Code Generation Pipeline research doc.
The canonical source of truth is the emitter at
modules/codegen/src/main/scala/specrest/codegen/go/EmitGo.scala, which renders the Handlebars
templates under modules/codegen/src/main/resources/templates/go/chi/. Running
sbt "cli/run compile --framework chi --db postgres --ignore-verify --out /tmp/out fixtures/spec/url_shortener.spec"produces the tree this page describes. If this page and the emitter disagree, the emitter wins; file
an issue or PR to correct the doc. The full byte-level shape of the output for url_shortener.spec
is checked in under fixtures/golden/codegen/go/chi/postgres/url_shortener/ and asserted by
EmitGoTest.
At a glance
| Aspect | Value |
|---|---|
| Language | Go 1.22 |
| Framework | chi v5 (net/http) |
| ORM / driver | Bun (uptrace/bun) + bun/driver/pgdriver |
| Migration tool | golang-migrate (raw SQL under migrations/) |
| Database | PostgreSQL 16+ |
| Validation | go-playground/validator/v10 struct tags |
| Config | caarlos0/env/v11 (env vars) |
| HTTP server | net/http w/ chi middleware (RequestID, Logger, Recoverer, …) |
| Logging | log/slog (JSON handler) |
Project layout
Type mapping
| Spec type | Go type | PostgreSQL column |
|---|---|---|
String | string | TEXT |
Int | int64 | BIGINT |
Float | float64 | DOUBLE PRECISION |
Bool | bool | BOOLEAN |
DateTime | time.Time | TIMESTAMPTZ |
Date | time.Time | DATE |
UUID | uuid.UUID | UUID |
Decimal | decimal.Decimal | NUMERIC |
Bytes | []byte | BYTEA |
Money | int64 | BIGINT |
Option[T] | *T | nullable column |
Set[T] | []T | JSONB |
Seq[T] | []T | JSONB |
The mapping is in modules/profile/src/main/scala/specrest/profile/TypeMap.scala.
Operation routing
RouteKind.classify (target-agnostic) maps each operation to one of create, read, list,
delete, redirect, other. The Go emitter renders the matching handler/service template.
Operations whose body shape does not match the entity's field set route to other and emit a stub
the user fills in (the spec contract is preserved by the verify gate, so the stub still has a known
interface).
Observability
GET /health returns 200 unconditionally (liveness; the Dockerfile healthcheck probes it).
GET /ready pings the database through db.PingContext and answers 200 or 503.
GET /metrics is promhttp.Handler() from prometheus/client_golang, registered for GET
only. A trackRequests middleware records http_requests_total and
http_request_duration_seconds labelled by method, chi route pattern, and status; it sits
outside Recoverer in the middleware chain so a panicking request is still counted as the
500 Recoverer writes.
Tracing is opt-in. With OTEL_EXPORTER_OTLP_ENDPOINT set, otelhttp.NewHandler wraps the
router and exports OTLP/HTTP spans, renamed to METHOD /route/{pattern} once chi has routed;
unset, nothing is wired. OTEL_SERVICE_NAME overrides the default service name.
Dafny → Go integration
When compile --framework chi --db postgres --with-synthesis is invoked and the verified-body cache
is populated, the compiler:
- Routes through
dafny translate -go(viaTargetLanguage.Go). - Lays the produced Go files under
internal/dafnykernel/. - Emits operation bindings of the form
dafnykernel.{OperationName}.
The kernel is only emitted when at least one operation is classified as LLM_SYNTHESIS; otherwise
the Go project is purely CRUD/Bun.
Extension points
internal/extensions/extensions.go is scaffolded on first compile and preserved on every
subsequent compile. The generated cmd/server/main.go calls extensions.Register(r, db)
once, before wiring any spec-derived route. That ordering is mandatory: chi panics on
r.Use(...) after a route has been registered (chi: all middlewares must be defined before routes on a mux). Middleware installed here therefore wraps every generated
handler. Routes added here are overwritten by any generated route with the same
method + path (chi v5 silently replaces a duplicate; the spec wins on collision). Use it
for custom routes or middleware that should live next to the generated code but survive
regeneration:
package extensions
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/uptrace/bun"
)
func Register(r chi.Router, db *bun.DB) {
_ = db
r.Get("/custom/ping", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
}spec-to-rest compile --dry-run reports this file as preserve on every run after the
first. Any other manual edit under cmd/, internal/, or migrations/ is overwritten on
the next compile. In-file protected-region markers are intentionally not provided; the
sidecar package is the supported mechanism.
Multi-environment compose
The base docker-compose.yml ships next to three additional files, all rendered from
modules/codegen/src/main/scala/specrest/codegen/Compose.scala:
docker-compose.override.yml.example: local-dev hints. Copy todocker-compose.override.yml; Compose auto-loads it next to the base. Regenerated on every compile, your renamed copy is never touched.docker-compose.staging.yml:restart: on-failure, appmemory: 256M/cpus: 0.25, dbmemory: 512M/cpus: 0.5, every database secret required via${KEY:?…}. Scaffolded once, thenpreserve = true(the file is never regenerated so you can tune the limits).docker-compose.prod.yml:restart: unless-stopped, appmemory: 512M/cpus: 0.5, dbmemory: 1G/cpus: 1.0, no exposed db port, every secret required via${KEY:?…}. Same preserve semantics as staging.
Apply an overlay with -f layering:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dFor SQLite the staging/prod overlays only harden the app service (no db service to
harden); the override.example still ships.
CI gate
.github/workflows/go-build.yml is a {postgres, sqlite, mysql} matrix. On every PR that touches
the Go templates, profile, emitter, or the shared migration renderer it re-runs
compile --framework chi --db <dialect>, then go mod tidy && go vet && go build against the
emitted project and a golang-migrate round-trip (up → down -all → up) against a real
postgres:17 / mysql:8.4 service (SQLite uses a file), so the emitted migrations/*.sql is
proven to apply and reverse on every supported dialect, not just that the Go code compiles.
Test generation
Conformance / property / stateful test generation is on by default (opt out with
--no-tests) for go-chi-* on every dialect (postgres, sqlite, mysql), emitted in
the target's own
language: go test + rapid property tests, not
Python. testgen plugs the Go renderers into the shared Backend.scala seam
(ExprBackend / StrategyBackend / HarnessTemplates); the same spec-derived
derivation engine feeds the Python (fastapi) path, which stays the byte-identical
differential oracle (#278,
#279,
#280; closes
#265).
The emitted suite lives under tests/ (Go package tests, build-tagged conformance):
behavioral (<svc>_behavioral_test.go, positive-ensures under rapid.Check), stateful
(<svc>_stateful_test.go, random operation sequences asserting invariants per step),
and structural-lite (<svc>_structural_test.go, fuzzes every non-stub operation with
type-valid input and asserts no 5xx, schemathesis's core check minus full schema
validation; fail-loud stubs are honest-skipped exactly as the Python schemathesis
renderer excludes them, recorded in tests/_testgen_skips.json). The
/admin/* reset/state/seed router is a spec-derived internal/admin/admin.go,
compiled into every build and guarded by bearer auth: with no ADMIN_TOKEN
configured (the production default) the routes answer 404; with one configured,
requests need Authorization: Bearer $ADMIN_TOKEN. The conformance build tag
remains only on the test files themselves, as suite selection: go test ./...
without the tag never runs the live-server suite.
Run it against a plain production build:
go build -o server ./cmd/server
export ADMIN_TOKEN=$(openssl rand -hex 32)
./server &
bash tests/run_conformance.sh smoke # or: go test -tags conformance ./tests/...go-chi -> conformance suite runs this in CI (go-build.yml) across all four
fixtures × postgres/sqlite, mirroring the python-build.yml pattern. (The
spec-to-rest test CLI wrapper still drives only the Python runner; see
CLI Reference.)