DEV Community

Ara Israelyan
Ara Israelyan

Posted on

Less boilerplate than raw SQL, less magic than ORM: generating PostgreSQL queries from Go structs

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),
)
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

https://pkg.go.dev/github.com/juggle73/norm/v4

Top comments (0)