A lightweight, type-safe ORM for Go — powered by code generation and generics, with zero runtime reflection.
- Code generation — generates type-specific query helpers from your struct definitions
- Type-safe queries — built on Go generics (
Query[T]) - Zero reflection — all scanning, column mapping, and preloading are resolved at compile time
- Immutable query builder — every builder method returns a new
Query, safe to reuse - MySQL & PostgreSQL — dialect abstraction handles placeholder style, identifier quoting, and
RETURNING - Relations —
has_many,has_one,belongs_to,many_to_manywith eager loading (Preload) and JOIN support - Scopes — composable, reusable query fragments (
Where,OrderBy,Limit,Offset,In) - Transactions —
DB.Transactionwith automatic commit/rollback/panic-recovery
ormgen is for developers who want to write SQL-aware Go code without the boilerplate.
| gorm | sqlc | ormgen | |
|---|---|---|---|
| Approach | Runtime reflection | SQL-first codegen | Struct-first codegen |
| Input | Go structs + conventions | .sql files |
Go structs + tags |
| Magic | Hooks, soft delete, auto-timestamps | None | None |
| Debugging | Trace callbacks | Read the SQL | Read the generated Go code |
Why not gorm?
gorm is powerful and batteries-included. But its implicit behaviors — auto-timestamps, soft delete when DeletedAt
exists, hook chains, association auto-save — can make it hard to predict what SQL actually runs. ormgen generates all
query logic as plain Go code you can open and read.
Why not sqlc?
sqlc is great if you prefer writing raw SQL. But sqlc generates one Go function per SQL query — every combination of
JOINs, filters, and sort orders requires a separate .sql file. As requirements grow, you end up with GetUser,
GetUserWithPosts, GetUserWithProfile, ListUsersByRole, and so on. ormgen solves this with composable Scopes: a
single FindAll(ctx, scopes...) method covers all of these combinations at the call site.
What ormgen is not:
- Not a migration tool — use other tools, or plain SQL
- Not a full-featured ORM — no auto-timestamps, no soft delete, no callback hooks
- Not magic — if something happens, it's because your code explicitly asked for it
go install github.com/mickamy/ormgen@latestRequires Go 1.24+.
package model
import "time"
//go:generate ormgen -source=$GOFILE
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
Posts []Post `rel:"has_many,foreign_key:user_id"`
Profile *Profile `rel:"has_one,foreign_key:user_id"`
}
type Post struct {
ID int
UserID int
Title string
Body string
User *User `rel:"belongs_to,foreign_key:user_id"`
}- Exported fields are automatically mapped to snake_case columns (
CreatedAt->created_at) - A field named
IDis assumed to be the primary key - Struct, pointer-to-struct, and slice fields are automatically skipped as DB columns
go generate ./...This creates user_query_gen.go alongside the source file containing:
Users(db) *orm.Query[User]— factory functionPosts(db) *orm.Query[Post]— factory function- Per-type scan, column-value, set-PK, and preloader helpers
To generate into a separate package:
//go:generate ormgen -source=$GOFILE -destination=../querypackage main
import (
"context"
"database/sql"
_ "github.com/go-sql-driver/mysql"
"yourapp/model"
"yourapp/query"
"github.com/mickamy/ormgen/orm"
"github.com/mickamy/ormgen/scope"
)
func main() {
sqlDB, _ := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/mydb?parseTime=true")
db := orm.New(sqlDB, orm.MySQL) // or orm.PostgreSQL
ctx := context.Background()
// Create
u := &model.User{Name: "Alice", Email: "alice@example.com"}
query.Users(db).Create(ctx, u)
// u.ID is now populated
// Find
user, _ := query.Users(db).Where("id = ?", u.ID).First(ctx)
// Find all with scopes
users, _ := query.Users(db).
Scopes(scope.Where("name LIKE ?", "A%"), scope.Limit(10)).
OrderBy("id").
All(ctx)
// Preload relations
users, _ = query.Users(db).Preload("Posts").Preload("Profile").All(ctx)
// Join
users, _ = query.Users(db).Join("Posts").Select("DISTINCT users.*").All(ctx)
// Count / Exists
count, _ := query.Users(db).Count(ctx)
exists, _ := query.Users(db).Where("email = ?", "alice@example.com").Exists(ctx)
// Update
user.Name = "Alice Updated"
query.Users(db).Update(ctx, &user)
// Delete
query.Users(db).Where("id = ?", u.ID).Delete(ctx)
// Batch insert
posts := []*model.Post{
{UserID: u.ID, Title: "Post 1", Body: "body"},
{UserID: u.ID, Title: "Post 2", Body: "body"},
}
query.Posts(db).CreateAll(ctx, posts)
// Upsert
query.Posts(db).Upsert(ctx, posts[0])
// Transaction
db.Transaction(ctx, func(tx *orm.Tx) error {
query.Users(tx).Create(ctx, &model.User{Name: "Bob"})
return nil // commit; return error to rollback
})
}| Tag | Behavior |
|---|---|
| (no tag) | Column inferred from field name (CreatedAt -> created_at) |
db:"col_name" |
Explicit column name |
db:",primaryKey" |
Mark as primary key (default: field named ID) |
db:"-" |
Exclude from DB columns |
| Relation | Field type | Tag |
|---|---|---|
| has_many | []Post |
rel:"has_many,foreign_key:user_id" |
| has_one | *Profile |
rel:"has_one,foreign_key:user_id" |
| belongs_to | *User |
rel:"belongs_to,foreign_key:user_id" |
| many_to_many | []Tag |
rel:"many_to_many,join_table:user_tags,foreign_key:user_id,references:tag_id" |
| Method | Description |
|---|---|
Where(clause, args...) |
Add WHERE condition |
OrderBy(clause) |
Add ORDER BY |
Limit(n) |
Set LIMIT |
Offset(n) |
Set OFFSET |
Select(columns) |
Override SELECT columns |
Join(name) |
INNER JOIN on named relation |
LeftJoin(name) |
LEFT JOIN on named relation |
Preload(name) |
Eager load named relation |
Scopes(scopes...) |
Apply reusable scope objects |
| Method | Description |
|---|---|
All(ctx) |
([]T, error) — fetch all matching rows |
First(ctx) |
(T, error) — fetch first row (orm.ErrNotFound if none) |
Count(ctx) |
(int64, error) — count matching rows |
Exists(ctx) |
(bool, error) — check if any row matches |
Create(ctx, *T) |
Insert and populate PK |
CreateAll(ctx, []*T) |
Batch insert and populate PKs |
Upsert(ctx, *T) |
Insert or update on PK conflict |
Update(ctx, *T) |
Update by PK |
Delete(ctx) |
Delete matching rows (requires WHERE) |
Scopes are composable, reusable query fragments:
import "github.com/mickamy/ormgen/scope"
// Reusable scope
active := scope.Where("active = ?", true)
recent := scope.OrderBy("created_at DESC")
page := scope.Combine(scope.Limit(20), scope.Offset(0))
users, _ := query.Users(db).Scopes(active, recent).Scopes(page...).All(ctx)
// Generic In
ids := []int{1, 2, 3}
users, _ := query.Users(db).Scopes(scope.In("id", ids)).All(ctx)Without scopes, repositories tend to grow like this:
func (r *UserRepo) Get(ctx context.Context, id string) (User, error)
func (r *UserRepo) GetWithPosts(ctx context.Context, id string) (User, error)
func (r *UserRepo) GetWithProfile(ctx context.Context, id string) (User, error)
func (r *UserRepo) GetForSignIn(ctx context.Context, id string) (User, error)
func (r *UserRepo) ListAdmins(ctx context.Context, page int) ([]User, error)
func (r *UserRepo) ListRecent(ctx context.Context, limit int) ([]User, error)
// ... and so on for every combinationWith scopes, a single method covers all of these:
type UserRepo struct{ db orm.Querier }
func (r *UserRepo) Get(ctx context.Context, id int, scopes ...scope.Scope) (User, error) {
return query.Users(r.db).Where("id = ?", id).Scopes(scopes...).First(ctx)
}
func (r *UserRepo) List(ctx context.Context, scopes ...scope.Scope) ([]User, error) {
return query.Users(r.db).Scopes(scopes...).OrderBy("id").All(ctx)
}The caller decides how to filter:
// Just the user
user, _ := repo.Get(ctx, 1)
// List with filtering + pagination
users, _ := repo.List(ctx,
scope.Where("role = ?", "admin"),
scope.OrderBy("created_at DESC"),
scope.Limit(20),
scope.Offset(page * 20),
)ormgen -source=<path> [-destination=<dir>] [-version]
| Flag | Description |
|---|---|
-source |
Source .go file (required) |
-destination |
Output directory (default: same as source) |
-version |
Print version |
Table names are auto-inferred: User -> users, UserProfile -> user_profiles.
# Start MySQL & PostgreSQL
make up-d
# Run unit tests
make test
# Run integration tests (requires running DBs)
make itest
# Lint
make lint