GERPO (Golang + Repository) is a generic repository pattern for Go with pluggable adapters and a tiny footprint. It is not an ORM — no migrations, no relations, no struct tags. All SQL behavior is declared once in the repository configuration; columns are bound to struct fields through pointers.
Database support. gerpo currently targets PostgreSQL (and PG-compatible databases such as CockroachDB). The SQL fragments gerpo emits — placeholder format, LIKE type-casts,
RETURNING, window functions — assume PostgreSQL. MySQL, MS SQL Server, and pre-3.35 SQLite are not supported today. SeeTODO.mdfor the multi-dialect backlog.
📚 Full documentation: insei.github.io/gerpo · Why gerpo? (vs GORM / ent / bun / sqlc / sqlx) · API reference: pkg.go.dev/github.com/insei/gerpo
go get github.com/insei/gerpo@latestMinimum Go version: 1.24.
type User struct {
ID uuid.UUID
Name string
Email *string
Age int
CreatedAt time.Time
}
repo, err := gerpo.New[User]().
Adapter(pgx5.NewPoolAdapter(pool)).
Table("users").
Columns(func(m *User, c *gerpo.ColumnBuilder[User]) {
c.Field(&m.ID).OmitOnUpdate()
c.Field(&m.Name)
c.Field(&m.Email)
c.Field(&m.Age)
c.Field(&m.CreatedAt).OmitOnUpdate()
}).
Build()
users, _ := repo.GetList(ctx, func(m *User, h query.GetListHelper[User]) {
h.Where().Field(&m.Age).GTE(18)
h.OrderBy().Field(&m.CreatedAt).DESC()
h.Page(1).Size(20)
})Full runnable sample lives in examples/todo-api/ — a CRUD REST service with PostgreSQL, goose migrations and docker-compose wiring. Additional end-to-end scenarios are in the integration tests.
| Area | Highlights | Docs |
|---|---|---|
| Repository | Type-safe builder, thread-safe, sync.Pool backed statements |
Repository builder |
| Columns | AsColumn / AsVirtual, insert/update protection, aliases |
Columns, Virtual columns |
| Queries | 14 WHERE operators + IC variants, AND/OR/Group, ordering, pagination | WHERE operators, Ordering & pagination |
| Operations | GetFirst / GetList / Count / Insert / InsertMany / Update / Delete with Only / Exclude |
CRUD operations, Exclude & Only |
| Persistent queries | Always-on WHERE, JOIN, GROUP BY via WithQuery |
Persistent queries |
| Soft delete | Rewrite DELETE as UPDATE of a marker field | Soft delete |
| Hooks | Before/After for Insert/Update, AfterSelect | Hooks |
| Transactions | gerpo.WithTx(ctx, tx) / gerpo.RunInTx share one tx across every Repository bound to the same context |
Transactions |
| Cache | Context-scoped cache out of the box, pluggable backend | Cache |
| Error handling | WithErrorTransformer maps gerpo errors to domain errors |
Error transformer |
gerpo talks to a database through an executor.Adapter — a thin wrapper around an underlying SQL driver. gerpo targets PostgreSQL today; all three bundled adapters wrap PostgreSQL drivers:
| Adapter | Package | Wraps driver | Placeholders |
|---|---|---|---|
| pgx v5 | executor/adapters/pgx5 |
github.com/jackc/pgx/v5 |
$1, $2, … |
| pgx v4 | executor/adapters/pgx4 |
github.com/jackc/pgx/v4 |
$1, $2, … |
| database/sql | executor/adapters/databasesql |
any database/sql driver — pair with a PG driver (pq, pgx/stdlib) |
? or $1 (configurable) |
PG-compatible databases (CockroachDB, MariaDB ≥10.5, SQLite ≥3.35) are likely to work as drop-in — not formally tested. MySQL, MS SQL Server, and older SQLite are not supported: gerpo's LIKE CAST(? AS text), INSERT … RETURNING, and window-function COUNT(*) OVER () all assume PG. See TODO.md.
Writing a custom adapter is three methods (ExecContext, QueryContext, BeginTx) — see Adapters and adapter internals.
- SQL lives only in the repository configuration.
- Columns are bound to struct fields through pointers.
- Entities carry no database markers (no tags, no interfaces).
- gerpo does not implement relations between entities.
- gerpo does not modify the database schema.
Details and rationale: Ideology.
gerpo uses minimal reflection and pools statement objects to keep allocations under control. Two views of the overhead — a mock adapter isolates the framework cost, a real PostgreSQL shows the cost a caller actually experiences with network round-trip in the picture.
Against real PostgreSQL. make bench-report-pg spins up an isolated postgres:16 in Docker, applies the bench schema, runs every CRUD op paired (pgx v5 pool vs gerpo repo), and tears the stack down. Sample run on a local machine:
| Op | Direct ns/op | Gerpo ns/op | × ns | × B | × allocs |
|---|---|---|---|---|---|
| GetFirst | 59 804 | 66 878 | 1.1× | 2.0× | 1.5× |
| GetList | 84 030 | 100 375 | 1.2× | 1.2× | 1.1× |
| Count | 105 780 | 162 432 | 1.5× | 2.6× | 2.9× |
| Insert | 1 607 957 | 1 638 373 | 1.0× | 2.4× | 2.0× |
| Update | 1 488 061 | 1 621 205 | 1.1× | 3.1× | 2.6× |
| Delete | 58 162 | 63 522 | 1.1× | 2.3× | 2.0× |
Reads and Delete-on-miss come out at roughly +10 % latency. INSERT / UPDATE sit at ~1.6 ms per call on a local PG — that is a real fsync on commit, not framework overhead; the gerpo layer contributes ~30 µs on top. Count is the outlier at +50 % because a trivial SELECT count(*) WHERE age >= ? is so cheap that gerpo's fixed per-call cost is visible as a percentage; it shrinks on non-trivial queries. Allocation ratios reflect the price of generic SQL generation and struct-field mapping.
Against a mock adapter (IO = 0, make bench-report) the ratios are larger — the framework cost is no longer amortised by network. Per-op absolute cost stays in the 0.5–1.5 µs band, which is what survives on real traffic.
WHERE operators (EQ, In, Contains, …) take any, so the compiler cannot
catch h.Where().Field(&m.Age).EQ("18") — field is int, argument is a
string — until runtime. gerpo ships a go/analysis checker that catches
these mismatches at go vet time.
go install github.com/insei/gerpo/cmd/gerpolint@latest
gerpolint ./...
# …or from a clone:
make lint-gerpolintRules (GPL001..GPL005): scalar type mismatch, variadic element mismatch,
string-only operator on non-string field, unresolved field pointer, and
any-typed argument. Silence specific lines with //gerpolint:disable-line,
//gerpolint:disable-next-line[=GPL001,…], or the
//gerpolint:disable / //gerpolint:enable block pair.
Using gerpolint as a golangci-lint plugin. Drop the repo's
.custom-gcl.yml into your project (pointing
module: github.com/insei/gerpo, import: github.com/insei/gerpo/gerpolintplugin),
add gerpolint to your linters config, and build a bespoke binary:
golangci-lint custom # produces ./bin/custom-gcl with gerpolint embedded
./bin/custom-gcl run ./...# .golangci.yml
linters:
enable: [gerpolint]
settings:
custom:
gerpolint:
type: module
settings:
unresolved-field: skip # skip | warn | error
any-arg: warn # skip | warn | error
disabled-rules: [] # [GPL001, GPL002, …]1.0.0
- Caching engine configuration in the repository builder (#46).
- New API for configuring virtual columns —
Compute(sql, args...)replacedWithSQL;Aggregate()marks aggregate expressions;Filter(op, spec)registers per-operator overrides.
The API is now stable and ready for v1.0.0.
-
Unit tests:
go test ./... -
Integration tests (Docker required):
docker compose -f tests/integration/docker-compose.yml up -d GERPO_INTEGRATION_DB_URL="postgres://gerpo:gerpo@localhost:5433/gerpo?sslmode=disable" \ go test -tags=integration ./tests/integration/...
-
Every PR runs a mock-db benchmark diff via
benchstatand posts the summary as a PR comment.
More in Contributing.
MIT — see LICENSE.md.