Grain
Grain is a type-safe Object-Relational Mapping (ORM) for Go, built on
pgx. It targets PostgreSQL only.
- Type-safe schema — tables and columns defined once in Go; column binds
are checked at startup, not on first SQL error.
- Snapshot-driven migrations — edit your schema, then
grain generate
diffs it against the last applied snapshot and writes the SQL for you.
- Fluent query builder —
SELECT/INSERT/UPDATE/DELETE with joins,
conditions, grouping and pagination, parameterized everywhere.
- Transactions — one method that begins, commits and rolls back for you.
- Reflection-based scanning — rows map into plain structs via
db: tags.
Contents
Installation
Add the library and the grain CLI to your project:
go get github.com/etornam45/grain@latest
go install github.com/etornam45/grain/cmd/grain@latest # CLI -> $(go env GOPATH)/bin/grain
The CLI is a separate binary from the library — you only need it if you manage
migrations with grain generate / grain migrate.
A runnable version of everything below lives in examples/basic.
Usage Example
import "github.com/etornam45/grain/schema"
var UserStatus = schema.Enum("user_status", "active", "suspended", "banned")
type usersColumns struct {
ID, Name, Email, Age, Status *schema.ColumnDef
}
var Users = schema.Table[usersColumns]("users",
schema.Column("id", schema.UUID()).PrimaryKey().Default("gen_random_uuid()"),
schema.Column("name", schema.Varchar(255)).NotNull(),
schema.Column("email", schema.Varchar(255)).NotNull().Unique(),
schema.Column("age", schema.Int()),
schema.Column("status", UserStatus).NotNull().Default("active"),
)
Migrations
Generate a migration from your schema code, then apply it:
grain generate -schema path/to/model "create user table"
DATABASE_URL=postgres://user:pass@localhost:5432/app grain migrate up
Command usage
usage:
grain generate -schema <dir> [name]
grain migrate up (requires DATABASE_URL)
grain migrate down (requires DATABASE_URL)
grain migrate status (requires DATABASE_URL)
Querying
These snippets assume a table defined as in the Usage example,
plus ctx, conn, tx and newID in scope. query is
github.com/etornam45/grain/query and db is
github.com/etornam45/grain/db.
- Inserting
var newID string
err = query.Insert(schema.Users).
Values(map[string]any{
"name": "Ama",
"email": "ama@example.com",
"status": "active",
}).
Returning(schema.Users.Cols.ID.String()).
Scan(ctx, conn, &newID)
- Selecting
type User struct {
ID string `db:"users.id"`
Name string `db:"users.name"`
Email string `db:"users.email"`
Status string `db:"users.status"`
}
users, err := query.Select[User](schema.Users.Cols.ID, schema.Users.Cols.Name, schema.Users.Cols.Email, schema.Users.Cols.Status).
From(schema.Users).
Where(query.Eq(schema.Users.Cols.Status, "active")).
OrderBy([]string{schema.Users.Cols.Name.Str()}, query.Asc).
Limit(10).
All(ctx, conn)
- Joins
rows, err := query.Select[UserOrderRow](schema.Users.Cols.Name, schema.Orders.Cols.Total).
From(schema.Users).
InnerJoin(schema.Orders, query.Eq(schema.Users.Cols.ID, schema.Orders.Cols.UserID)).
Where(query.Gt(schema.Orders.Cols.Total, 100)).
All(ctx, conn)
- Deleting
_, err = query.Delete(schema.Users).Where(query.Eq(schema.Users.Cols.ID, newID)).Run(ctx, tx)
- Transactions
err = conn.Transaction(ctx, func(tx *db.Tx) error {
_, err := query.Delete(schema.Orders)
.Where(query.Eq(schema.Orders.Cols.UserID, newID))
.Run(ctx, tx)
if err != nil {
return err
}
_, err = query.Delete(schema.Users)
.Where(query.Eq(schema.Users.Cols.ID, newID))
.Run(ctx, tx)
return err
})
Documentation
Full library reference and tutorials live in docs/:
- Schema — tables, columns, types, enums, foreign keys, indexes
- Query — select/insert/update/delete, conditions, joins, aggregation
- db & scan — connections, transactions, row mapping
- Migrations & CLI — the
grain CLI, generation, apply/rollback/status
- Tutorials — getting started, relationships
Requirements
License
Grain is released under the Apache License 2.0. See LICENSE for details.