I like writing SQL manually in Go.
Especially with pgx or database/sql, it gives you explicit control over what happens: no hidden queries, no entity tracking, no unexpected ORM behavior.
But after writing enough services, the same boilerplate starts to repeat everywhere:
- column lists
- $1, $2, $3 placeholders
- INSERT / UPDATE field mapping
- scan pointers
- simple dynamic WHERE clauses
- small schema sync helpers for internal tools and MVPs
So I built norm.
GitHub: https://github.com/juggle73/norm
Docs: https://pkg.go.dev/github.com/juggle73/norm/v4
What norm is
norm is a lightweight PostgreSQL-focused SQL generation helper for Go structs.
It is intentionally not an ORM.
It does not:
- execute queries
- manage connections
- track entities
- lazy-load relations
- hide SQL from you
It only generates SQL and args from Go structs, so you can still execute everything yourself with pgx, database/sql, or any other database layer.
The goal is simple:
Keep raw SQL control, but remove repetitive SQL boilerplate.
Small example
type User struct {
Id int64 norm:"pk"
Name string norm:"notnull"
Email string
}
m, _ := orm.M(&user)
sql, args, err := m.Insert(
norm.Exclude(“id”),
norm.Returning(“Id”),
)
You get generated SQL and args, then execute them yourself.
Example generated SQL:
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id
Why not just use raw SQL?
Raw SQL is still the best choice for many Go projects.
But this gets annoying:
row := db.QueryRow(ctx, `INSERT INTO users ( name, email, status, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
user.Name,
user.Email,
user.Status,
user.CreatedAt,
user.UpdatedAt,
)
Every time the struct changes, you have to update:
- the column list
- the placeholder list
- the args list
- sometimes the scan list
- sometimes several SELECT / INSERT / UPDATE statements
norm tries to remove that specific pain without taking ownership of your database access layer.
Why not use an ORM?
ORMs are useful, but sometimes I do not want:
- hidden queries
- relationship magic
- hooks
- entity lifecycle
- implicit transactions
- complex abstractions around SQL
In many backend services, I want SQL to remain visible and explicit.
That is why norm does not try to replace GORM, Bun, Ent, or sqlc.
It targets a smaller gap:
More convenient than handwritten repetitive SQL, but much less abstract than a full ORM.
Why not use sqlc?
sqlc is excellent when you want to write SQL files and generate type-safe Go code.
But sometimes I do not want a generation step.
Sometimes I want small dynamic SQL fragments built from Go structs at runtime.
Different tool, different trade-off.
Why not use squirrel or goqu?
Query builders are good for dynamic SQL.
But they usually do not solve struct-driven boilerplate directly:
- struct fields to columns
- INSERT from struct
- UPDATE from struct
- scan pointers from struct
- schema diff helpers from struct tags
That is the part norm focuses on.
Current features
norm can generate:
- SELECT queries
- INSERT queries
- UPDATE queries
- DELETE queries
- WHERE fragments
- field lists
- placeholders
- scan pointers
- JOIN helpers
- lightweight migration sync/diff helpers
It supports PostgreSQL-style placeholders and is designed to work well with pgx and database/sql.
Example use case
A typical use case is an internal backend service where you have structs like this:
type Product struct {
Id int64 norm:"pk"
Name string norm:"notnull"
Sku string norm:"unique"
Price int64
CreatedAt time.Time
}
You want to keep SQL execution explicit, but you do not want to manually write the same column mapping logic for every CRUD operation.
That is where norm helps.
Scope
The scope is intentionally limited.
norm is not trying to be:
- a full ORM
- a migration framework replacement
- a database abstraction layer
- a cross-database SQL dialect system
For now, it is PostgreSQL-focused and Go-focused.
Feedback wanted
I would appreciate honest technical feedback:
- Does this API feel idiomatic for Go?
- Is the “not an ORM, just SQL generation” scope clear enough?
- What would make this unsafe or inconvenient in real PostgreSQL projects?
- Which feature would matter most: UPSERT, better joins, better migrations, or more examples?
- Would you use something like this with pgx?
Repository:
https://github.com/juggle73/norm
Documentation:
Top comments (0)