norm

package module
v4.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 2, 2026 License: MIT Imports: 8 Imported by: 0

README

norm - SQL query helper for Go structs

norm is a lightweight library that simplifies building SQL queries from Go structs for PostgreSQL. It is not an ORM — it does not execute queries or manage connections. Instead, it generates SQL fragments (field lists, bind parameters, WHERE conditions) that you compose into queries yourself. Works with any PostgreSQL driver (pgx, lib/pq, etc.).

Table of contents

Install

go get github.com/juggle73/norm/v4

Quick start

package main

import (
    "context"
    "database/sql"

    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/juggle73/norm/v4"
    "github.com/juggle73/norm/v4/migrate"
)

type User struct {
    Id    int64  `norm:"pk"`
    Name  string `norm:"notnull"`
    Email string
}

func main() {
    ctx := context.Background()
    pool, _ := pgxpool.New(ctx, "postgres://localhost/mydb")

    orm := norm.NewNorm(nil)

    // Register models
    orm.AddModel(&User{}, "users")

    // Sync tables (create if not exist, add missing columns)
    db, _ := sql.Open("pgx", "postgres://localhost/mydb")
    mig := migrate.New(db, orm)
    _ = mig.Sync(ctx)

    // SELECT
    user := User{}
    m, _ := orm.M(&user)
    sql, args, _ := m.Select(norm.Where("id = ?", 42))
    // → "SELECT id, name, email FROM users WHERE id=$1"
    _ = pool.QueryRow(ctx, sql, args...).Scan(m.Pointers()...)

    // INSERT
    newUser := User{Name: "Alice", Email: "alice@example.com"}
    m, _ = orm.M(&newUser)
    sql, vals, _ := m.Insert(norm.Exclude("id"), norm.Returning("Id"))
    // → "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"
    _ = pool.QueryRow(ctx, sql, vals...).Scan(m.Pointer("Id"))

    // UPDATE
    user.Name = "Bob"
    m, _ = orm.M(&user)
    sql, args, _ = m.Update(norm.Exclude("id"), norm.Where("id = ?", user.Id))
    // → "UPDATE users SET name=$1, email=$2 WHERE id=$3"
    _, _ = pool.Exec(ctx, sql, args...)

    // DELETE
    sql, args, _ = m.Delete(norm.Where("id = ?", user.Id))
    // → "DELETE FROM users WHERE id=$1"
    _, _ = pool.Exec(ctx, sql, args...)
}

Core concepts

Norm instance

Norm is the entry point. It caches struct metadata so reflection only happens once per type.

orm := norm.NewNorm(nil) // default config

orm := norm.NewNorm(&norm.Config{
    DefaultString: "varchar",      // default: "text"
    DefaultTime:   "timestamp",    // default: "timestamptz"
    DefaultJSON:   "json",         // default: "jsonb"
    JSONMarshal:   sonic.Marshal,  // default: encoding/json
    JSONUnmarshal: sonic.Unmarshal,
})
Model

Model is a lightweight wrapper that binds cached metadata to a specific struct instance. Each call to M() returns a new Model bound to the given pointer.

user := User{Id: 1, Name: "John"}
m, err := orm.M(&user)
// m.Values()   → reads from &user
// m.Pointers() → points into &user

Model is not safe for concurrent use. Metadata caching is thread-safe, but each goroutine should get its own Model via M().

AddModel

Use AddModel to register a struct with a custom table name:

orm.AddModel(&User{}, "app_users") // table name = "app_users"

Without AddModel, M() auto-generates the table name from the struct name in snake_case (User -> user, UserProfile -> user_profile).

Field and table naming

All names are automatically converted to snake_case. This applies to both table names and column names:

  • Struct UserProfile → table user_profile
  • Field UserName → column user_name
  • Field CreatedAt → column created_at

If your database uses a different naming convention, use dbName tag to override:

type User struct {
    UserName string                     // → column "user_name"
    UserName string `norm:"dbName=username"` // → column "username"
}

Table names can be overridden with AddModel:

orm.AddModel(&User{}, "app_users") // table "app_users" instead of "user"

Important: norm will not match fields to columns automatically if they don't follow snake_case convention. If your column is username but your field is UserName (which maps to user_name), you must add norm:"dbName=username".

Struct tags

Fields are configured via the norm struct tag:

type User struct {
    Id        int       `norm:"pk"`
    Email     string    `norm:"unique,notnull"`
    Name      string    `norm:"dbName=full_name"`
    Role      string    `norm:"notnull,default='user'"`
    Data      string    `norm:"dbType=jsonb"`
    Internal  string    `norm:"-"`          // skip this field
    CreatedAt time.Time                     // no tag = auto snake_case name
}
Tag Description
pk Mark as primary key (supports composite)
unique Add UNIQUE constraint
notnull Add NOT NULL constraint
default=value Set DEFAULT value
dbName=name Override column name (default: snake_case of field name)
dbType=type Override PostgreSQL type
fk=ModelName Mark as foreign key (accepts any format: UserType, userType, user_type)
- Skip field entirely

Embedded structs

norm supports Go struct embedding for sharing common fields:

type BaseModel struct {
    Id        int       `norm:"pk"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    BaseModel
    Name  string
    Email string `norm:"unique"`
}

// m.Fields() → "id, created_at, updated_at, name, email"

Both value and pointer embeddings are supported (BaseModel and *BaseModel).

JSON struct fields

Fields of type struct or *struct (except time.Time) are automatically marshaled to JSON when writing and unmarshaled when reading. No tags required:

type Address struct {
    City   string `json:"city"`
    Street string `json:"street"`
}

type User struct {
    Id      int     `norm:"pk"`
    Name    string
    Address Address // automatically handled as JSON
}

orm := norm.NewNorm(nil)

// INSERT — Address is marshaled to JSON bytes
user := User{Name: "Alice", Address: Address{City: "Moscow", Street: "Tverskaya"}}
m, _ := orm.M(&user)
sql, vals, _ := m.Insert(norm.Exclude("id"))
// vals = ["Alice", []byte(`{"city":"Moscow","street":"Tverskaya"}`)]

// SELECT — Address is unmarshaled from JSON
var loaded User
m, _ = orm.M(&loaded)
sql, args, _ = m.Select(norm.Where("id = ?", 1))
_ = pool.QueryRow(ctx, sql, args...).Scan(m.Pointers()...)
// loaded.Address.City == "Moscow"

Pointer struct fields (*Address) work the same way. nil pointers marshal to null.

By default encoding/json is used. For better performance, plug in a faster codec via Config:

import "github.com/bytedance/sonic"

orm := norm.NewNorm(&norm.Config{
    JSONMarshal:   sonic.Marshal,
    JSONUnmarshal: sonic.Unmarshal,
})

Query building

SELECT

The simplest way — use m.Select():

user := User{}
m, _ := orm.M(&user)

sql, args, _ := m.Select(norm.Where("id = ?", 42))
// sql = "SELECT id, name, email FROM users WHERE id=$1"

err := pool.QueryRow(ctx, sql, args...).Scan(m.Pointers()...)

With all options:

sql, args, _ := m.Select(
    norm.Exclude("password"),
    norm.Where("active = ?", true),
    norm.Order("Name DESC"),
    norm.Limit(10),
    norm.Offset(20),
)
// → "SELECT id, name, email FROM users WHERE active=$1 ORDER BY name DESC LIMIT 10 OFFSET 20"

You can also build SELECT manually with individual methods:

sql := fmt.Sprintf("SELECT %s FROM %s WHERE id=$1", m.Fields(), m.Table())
err := pool.QueryRow(ctx, sql, 42).Scan(m.Pointers()...)
INSERT

Use m.Insert() — returns SQL and values from the bound struct:

user := User{Name: "Alice", Email: "alice@example.com"}
m, _ := orm.M(&user)

sql, vals, _ := m.Insert(norm.Exclude("id"))
// sql  = "INSERT INTO users (name, email) VALUES ($1, $2)"
// vals = ["Alice", "alice@example.com"]

_, err := pool.Exec(ctx, sql, vals...)
INSERT with RETURNING
sql, vals, _ := m.Insert(norm.Exclude("id"), norm.Returning("Id"))
// → "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"

err := pool.QueryRow(ctx, sql, vals...).Scan(m.Pointer("Id"))
UPDATE

Use m.Update() — builds SET clause from bound struct values, chains bind numbers into WHERE:

user := User{Id: 1, Name: "Bob", Email: "bob@new.com"}
m, _ := orm.M(&user)

sql, args, _ := m.Update(norm.Exclude("id"), norm.Where("id = ?", user.Id))
// sql  = "UPDATE users SET name=$1, email=$2 WHERE id=$3"
// args = ["Bob", "bob@new.com", 1]

_, err := pool.Exec(ctx, sql, args...)

With RETURNING:

sql, args, _ := m.Update(
    norm.Exclude("id"),
    norm.Where("id = ?", user.Id),
    norm.Returning("Id"),
)
// → "UPDATE users SET name=$1, email=$2 WHERE id=$3 RETURNING id"

You can also build UPDATE manually with UpdateFields and BuildWhere:

set, nextBind := m.UpdateFields(norm.Exclude("id"))
whereStr, whereArgs := norm.BuildWhere(nextBind, "id = ?", user.Id)

sql := fmt.Sprintf("UPDATE %s SET %s WHERE %s", m.Table(), set, whereStr)
args := append(m.Values(norm.Exclude("id")), whereArgs...)
_, err := pool.Exec(ctx, sql, args...)
DELETE

Use m.Delete():

m, _ := orm.M(&User{})

sql, args, _ := m.Delete(norm.Where("id = ?", 42))
// sql  = "DELETE FROM users WHERE id=$1"
// args = [42]

_, err := pool.Exec(ctx, sql, args...)

With RETURNING:

sql, args, _ := m.Delete(norm.Where("id = ?", 42), norm.Returning("Id"))
// → "DELETE FROM users WHERE id=$1 RETURNING id"
JOIN

Use NewJoin to build SELECT queries across multiple tables. Fields are auto-prefixed with table names, and Pointers() collects scan targets from all models:

user := User{}
order := Order{}
mUser, _ := orm.M(&user)
mOrder, _ := orm.M(&order)

j := norm.NewJoin(mUser).
    Inner(mOrder, "orders.user_id = users.id").
    Where("users.active = ?", true).
    Order("users.name DESC").
    Limit(10)

sql, args, _ := j.Select()
// SELECT users.id, users.name, users.email, orders.id, orders.user_id, orders.total
//   FROM users
//   INNER JOIN orders ON orders.user_id = users.id
//   WHERE users.active=$1
//   ORDER BY users.name DESC LIMIT 10

err := pool.QueryRow(ctx, sql, args...).Scan(j.Pointers()...)
// user and order are populated

Multiple JOINs:

item := OrderItem{}
mItem, _ := orm.M(&item)

j := norm.NewJoin(mUser).
    Inner(mOrder, "orders.user_id = users.id").
    Left(mItem, "order_items.order_id = orders.id").
    Where("users.id = ?", 1)

sql, args, _ := j.Select()
err := pool.QueryRow(ctx, sql, args...).Scan(j.Pointers()...)

Supported join types: Inner, Left, Right.

Auto JOIN with FK tags

If your structs have fk tags, use Auto / AutoLeft to build JOINs without writing ON clauses:

type User struct {
    Id   int    `norm:"pk"`
    Name string
}

type Order struct {
    Id     int `norm:"pk"`
    UserId int `norm:"fk=User"`
    Total  int
}

type OrderItem struct {
    Id      int    `norm:"pk"`
    OrderId int    `norm:"fk=Order"`
    Product string
}

mUser, _ := orm.M(&user)
mOrder, _ := orm.M(&order)
mItem, _ := orm.M(&item)

j := norm.NewJoin(mUser).
    Auto(mOrder).       // → INNER JOIN order ON order.user_id = user.id
    AutoLeft(mItem)     // → LEFT JOIN order_item ON order_item.order_id = order.id

sql, args, _ := j.Select()
err := pool.QueryRow(ctx, sql, args...).Scan(j.Pointers()...)

Auto works in both directions — it finds the FK regardless of which model defines it. Panics if the relationship is ambiguous (multiple FKs to the same table) or missing — use Inner/Left/Right in those cases.

ORDER BY

Field names are validated against the model and converted to database column names:

m.OrderBy("Name ASC, Email DESC")
// → "name ASC, email DESC"

sql := fmt.Sprintf("SELECT %s FROM %s ORDER BY %s",
    m.Fields(), m.Table(), m.OrderBy("Name DESC"))

Accepts field names in any format (struct name, camelCase, snake_case). Direction is optional (defaults to ASC). Panics on unknown fields or invalid direction.

LIMIT / OFFSET
sql := fmt.Sprintf("SELECT %s FROM %s %s",
    m.Fields(), m.Table(),
    m.LimitOffset(10, 20),
)
// → "SELECT id, name, email FROM users LIMIT 10 OFFSET 20"
Extra scan targets

When your query returns columns not in the struct (e.g. computed columns):

var totalCount int
ptrs := m.Pointers(norm.AddTargets(&totalCount))
// ptrs = [&user.Id, &user.Name, &user.Email, &totalCount]

WHERE conditions builder

BuildConditions builds WHERE clauses from typed conditions:

m, _ := orm.M(&User{})

// Equality
conds, vals := m.BuildConditions(norm.Eq("name", "John"))
// conds = ["name=$1"], vals = ["John"]

// Comparison operators
conds, vals := m.BuildConditions(
    norm.Gte("age", 18),
    norm.Lt("age", 65),
)
// conds = ["age >= $1", "age < $2"], vals = [18, 65]

// IN clause
conds, vals := m.BuildConditions(norm.In("name", "Alice", "Bob"))
// conds = ["name IN ($1, $2)"], vals = ["Alice", "Bob"]

// LIKE
conds, vals := m.BuildConditions(norm.Like("name", "%john%"))
// conds = ["name LIKE $1"], vals = ["%john%"]

// IS NULL / IS NOT NULL
conds, vals := m.BuildConditions(norm.IsNull("email", true))
// conds = ["email IS NULL"], vals = []

// With table prefix (for JOINs) — inline or global
conds, vals := m.BuildConditions(norm.Eq("u.name", "John"))
// conds = ["u.name=$1"]

// Global prefix applies to all conditions without inline prefix
conds, vals := m.BuildConditions(norm.Eq("name", "John"), norm.Prefix("u."))
// conds = ["u.name=$1"]

// Mix different prefixes in one call
conds, vals := m.BuildConditions(
    norm.Eq("u.name", "John"),
    norm.Gte("o.total", 100),
)

// JSON field access
conds, vals := m.BuildConditions(norm.Eq("data->>key", "value"))
// conds = ["data->>key=$1"]

// Combine multiple conditions
conds, vals := m.BuildConditions(
    norm.Eq("name", "John"),
    norm.Gte("age", 18),
    norm.Lt("age", 65),
    norm.IsNull("deleted_at", true),
    norm.Prefix("u."),
)
Condition functions
Function SQL Example
Eq(field, value) field = $N norm.Eq("name", "John")
Gt(field, value) field > $N norm.Gt("age", 18)
Gte(field, value) field >= $N norm.Gte("age", 18)
Lt(field, value) field < $N norm.Lt("age", 65)
Lte(field, value) field <= $N norm.Lte("age", 65)
Ne(field, value) field != $N norm.Ne("status", "deleted")
Like(field, value) field LIKE $N norm.Like("name", "%john%")
IsNull(field, bool) field IS [NOT] NULL norm.IsNull("email", true)
In(field, values...) field IN ($N, ...) norm.In("id", 1, 2, 3)
Prefix(prefix) norm.Prefix("u.")

Code generation

Generate Go structs from an existing database schema. Code generation lives in a separate subpackage:

import (
    "database/sql"
    "github.com/juggle73/norm/v4/gen"
    _ "github.com/lib/pq" // or pgx/stdlib, etc.
)

db, _ := sql.Open("postgres", "postgres://localhost/mydb")
results, err := gen.FromDB(ctx, db, "models", "public")
// results is map[tableName]string with generated Go source code

for tableName, source := range results {
    os.WriteFile(tableName+".go", []byte(source), 0644)
}

FromDB accepts *sql.DB — any PostgreSQL driver works (lib/pq, pgx/stdlib, etc.).

Generated structs include norm tags (pk, notnull, unique, fk=...) detected from database constraints.

Migrations

Automatically create and alter tables based on your Go structs. Lives in the migrate subpackage:

import "github.com/juggle73/norm/v4/migrate"

orm := norm.NewNorm(nil)
orm.M(&User{})
orm.M(&Order{})

mig := migrate.New(db, orm)
Sync (development)

Creates missing tables and adds missing columns. Never drops or modifies existing columns — safe for development:

err := mig.Sync(ctx)
Diff (production)

Generates a full SQL diff for review — includes CREATE TABLE, ADD/DROP COLUMN, ALTER TYPE, SET/DROP NOT NULL:

sql, err := mig.Diff(ctx)
fmt.Println(sql)
// CREATE TABLE IF NOT EXISTS order (
//     id integer NOT NULL,
//     user_id integer NOT NULL,
//     total integer,
//     PRIMARY KEY (id),
//     FOREIGN KEY (user_id) REFERENCES user(id)
// );
// ALTER TABLE user ADD COLUMN IF NOT EXISTS age integer;
// ALTER TABLE user DROP COLUMN old_field;
CreateTableSQL

Preview the CREATE TABLE statement for any registered model:

fmt.Println(mig.CreateTableSQL("user"))

Go types are mapped to PostgreSQL types automatically. Use dbType tag to override:

Go type PostgreSQL type
int integer
int64 bigint
float64 double precision
string text (or Config.DefaultString)
bool boolean
time.Time timestamptz
struct jsonb
map[string]any jsonb
[]byte bytea

Options reference

Option Description Used by
Exclude("field1,field2") Exclude fields by db name Fields, Binds, UpdateFields, Pointers, Values
Fields("field1,field2") Include only these fields Fields, Binds, UpdateFields, Pointers, Values
Prefix("t.") Add table alias prefix Fields
Returning("field1,field2") Fields for RETURNING clause Insert, Update, Delete
Limit(n) LIMIT value Select
Offset(n) OFFSET value Select
Order("field [ASC|DESC]") ORDER BY clause Select
AddTargets(&var1, &var2) Extra scan targets Pointers
Where("field = ?", val) WHERE with ? placeholders Select, Update, Delete

Model methods reference

Method Returns Description
Fields(opts...) string Comma-separated column names
Binds(opts...) string Bind placeholders $1, $2, ...
UpdateFields(opts...) string, int SET clause + next bind number
Pointers(opts...) []any Field pointers for Scan
Values(opts...) []any Field values for Exec
Pointer(name) any Single field pointer
Table() string Table name
OrderBy(s) string Validated ORDER BY clause
Select(opts...) string, []any, error Full SELECT query + args
Insert(opts...) string, []any, error Full INSERT query + values
Update(opts...) string, []any, error Full UPDATE query + args
Delete(opts...) string, []any, error Full DELETE query + args
Returning(fields) string RETURNING clause
LimitOffset(limit, offset) string LIMIT/OFFSET clause
BuildConditions(conds...) []string, []any WHERE conditions from typed Cond values
FieldByName(name) *Field, bool Find field by any name format
FieldDescriptions() []*Field All field metadata
NewInstance() any New zero-value struct pointer

Join methods reference

Method Returns Description
NewJoin(base) *Join Create join builder with FROM model
Inner(m, on) *Join Add INNER JOIN
Left(m, on) *Join Add LEFT JOIN
Right(m, on) *Join Add RIGHT JOIN
Auto(m) *Join INNER JOIN with ON from FK tags
AutoLeft(m) *Join LEFT JOIN with ON from FK tags
Where(s, args...) *Join Set WHERE clause
Order(s) *Join Set ORDER BY (raw SQL)
Limit(n) *Join Set LIMIT
Offset(n) *Join Set OFFSET
Select() string, []any, error Build SELECT query
Pointers() []any Scan targets from all models

Norm methods reference

Method Returns Description
NewNorm(config) *Norm Create instance with optional config
M(&obj) *Model, error Get Model bound to struct instance
AddModel(&obj, table) *Model Register with explicit table name
Tables() []string All registered table names
FieldsByTable(table) []*Field Field descriptors for a table
GetConfig() *Config Current configuration

Migrate methods reference

Method Returns Description
New(db, orm) *Migrate Create migrate instance
Sync(ctx) error Create tables + add columns (safe)
Diff(ctx) string, error Full SQL diff for review
CreateTableSQL(table) string Preview CREATE TABLE statement

Benchmarks

SQL generation speed compared to popular query builders. Pure query building, no database involved.

Apple M1 Max, Go 1.23

SELECT
Library ns/op B/op allocs/op vs norm
norm 437 376 12 1.0x
squirrel 2749 3025 55 6.3x slower
goqu 2152 3368 78 4.9x slower
INSERT
Library ns/op B/op allocs/op vs norm
norm 740 416 18 1.0x
squirrel 2416 2425 53 3.3x slower
goqu 1738 2256 76 2.3x slower
UPDATE
Library ns/op B/op allocs/op vs norm
norm 911 608 23 1.0x
squirrel 3839 4138 81 4.2x slower
goqu 2754 3404 104 3.0x slower

norm is 3-6x faster with 3-9x fewer memory allocations. The advantage comes from cached struct metadata — reflection happens once per type, then all query building uses pre-computed field lists.

Run benchmarks yourself:

go test -bench=. -benchmem -run=^$

Important notes

  • M(&obj) returns a Model bound to that specific instance. Pointers(), Values(), and Pointer() work with the bound instance without additional parameters.
  • Model is not safe for concurrent use. Each goroutine should call M() to get its own Model.
  • Struct metadata is cached and shared safely across goroutines.
  • M() returns (*Model, error). Invalid argument (not a pointer to struct) returns an error.
  • OrderBy(), Returning(), and Pointer() panic on unknown field names - these are programmer errors.

Documentation

Overview

Package norm is a lightweight SQL query builder for Go structs.

It generates SQL fragments (field lists, bind parameters, WHERE conditions, full SELECT/INSERT/UPDATE/DELETE queries) from struct definitions using reflection. It is not an ORM — it does not execute queries or manage connections. You compose the generated SQL with any PostgreSQL driver (pgx, database/sql, etc.).

Quick start

type User struct {
    Id    int    `norm:"pk"`
    Name  string
    Email string
}

orm := norm.NewNorm(nil)
orm.AddModel(&User{}, "users")

// Sync tables (create/add columns)
mig := migrate.New(db, orm)
mig.Sync(ctx)

// SELECT
user := User{}
m, _ := orm.M(&user)
sql, args, _ := m.Select(norm.Where("id = ?", 42))
_ = pool.QueryRow(ctx, sql, args...).Scan(m.Pointers()...)

// INSERT
sql, vals, _ := m.Insert(norm.Exclude("id"))
_, _ = pool.Exec(ctx, sql, vals...)

Field and table naming

All names are automatically converted to snake_case:

UserProfile → user_profile (table)
UserName    → user_name   (column)
CreatedAt   → created_at  (column)

If your database column doesn't follow snake_case, override with dbName:

UserName string `norm:"dbName=username"` // → column "username"

Struct tags

Fields are configured via the "norm" struct tag:

pk           — mark as primary key
notnull      — NOT NULL constraint
unique       — UNIQUE constraint
default=val  — DEFAULT value
dbName=name  — override column name
dbType=type  — override PostgreSQL type
fk=Model     — foreign key (accepts CamelCase, camelCase, snake_case)
-            — skip field entirely

Configuration

Config controls default PostgreSQL types and JSON codec:

orm := norm.NewNorm(&norm.Config{
    DefaultString: "varchar",      // default: "text"
    DefaultTime:   "timestamp",    // default: "timestamptz"
    DefaultJSON:   "json",         // default: "jsonb"
    JSONMarshal:   sonic.Marshal,  // default: encoding/json
    JSONUnmarshal: sonic.Unmarshal,
})

Thread safety

Struct metadata is cached and shared safely across goroutines. Model is not safe for concurrent use — each goroutine should call Norm.M to get its own Model instance.

JSON fields

Struct and *struct fields (except time.Time) are automatically marshaled to JSON when writing (Model.Values, Model.Insert, Model.Update) and unmarshaled when reading (Model.Pointers).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Binds

func Binds(count int) string

Binds generates a bind placeholder string in "$1, $2, ..." format for the given number of parameters.

norm.Binds(3) // "$1, $2, $3"
norm.Binds(0) // ""

func BuildWhere

func BuildWhere(startBind int, where string, args ...any) (string, []any)

BuildWhere renders a WHERE clause string, replacing "?" placeholders with "$N" starting from startBind. Returns the rendered string and the args slice unchanged. Useful for building UPDATE queries manually.

set, nextBind := m.UpdateFields(norm.Exclude("id"))
whereStr, whereArgs := norm.BuildWhere(nextBind, "id = ?", user.Id)

func Prefix

func Prefix(prefix string) prefixOption

Prefix creates an option that adds a table alias prefix to field names. Also usable in [BuildConditions] to prefix all column references.

m.Fields(norm.Prefix("u.")) // "u.id, u.name, u.email"

Types

type ComposedOptions

type ComposedOptions struct {
	Exclude    []string
	Fields     []string
	Returning  []string
	Prefix     string
	Where      *whereOption
	AddTargets []any
	Offset     int
	Limit      int
	OrderBy    string
}

ComposedOptions holds the parsed result of all options passed to a method.

func ComposeOptions

func ComposeOptions(opts ...Option) ComposedOptions

ComposeOptions parses a list of Option values into a single ComposedOptions.

type Cond

type Cond interface {
	// contains filtered or unexported methods
}

Cond represents a single WHERE condition for [BuildConditions]. Use the constructor functions (Eq, Gt, Like, In, IsNull, etc.) to create conditions.

func Eq

func Eq(field string, value any) Cond

Eq creates an equality condition: field = value.

norm.Eq("name", "John")  // name=$1

func Gt

func Gt(field string, value any) Cond

Gt creates a greater-than condition: field > value.

norm.Gt("age", 18)  // age > $1

func Gte

func Gte(field string, value any) Cond

Gte creates a greater-or-equal condition: field >= value.

norm.Gte("age", 18)  // age >= $1

func In

func In(field string, values ...any) Cond

In creates an IN (...) condition.

norm.In("id", 1, 2, 3)          // id IN ($1, $2, $3)
norm.In("name", "Alice", "Bob") // name IN ($1, $2)

func IsNull

func IsNull(field string, isNull bool) Cond

IsNull creates an IS NULL or IS NOT NULL condition.

norm.IsNull("email", true)   // email IS NULL
norm.IsNull("email", false)  // email IS NOT NULL

func Like

func Like(field string, value any) Cond

Like creates a LIKE condition: field LIKE value.

norm.Like("name", "%john%")  // name LIKE $1

func Lt

func Lt(field string, value any) Cond

Lt creates a less-than condition: field < value.

norm.Lt("age", 65)  // age < $1

func Lte

func Lte(field string, value any) Cond

Lte creates a less-or-equal condition: field <= value.

norm.Lte("age", 65)  // age <= $1

func Ne

func Ne(field string, value any) Cond

Ne creates a not-equal condition: field != value.

norm.Ne("status", "deleted")  // status != $1

type Config

type Config struct {
	// DefaultString sets the PostgreSQL type used for Go string fields
	// when no dbType tag is specified. Defaults to "text".
	DefaultString string

	// DefaultTime sets the PostgreSQL type used for time.Time fields
	// when no dbType tag is specified. Defaults to "timestamptz".
	DefaultTime string

	// DefaultJSON sets the PostgreSQL type used for struct fields
	// serialized as JSON when no dbType tag is specified. Defaults to "jsonb".
	DefaultJSON string

	// JSONMarshal is the function used to marshal struct fields to JSON.
	// Defaults to [encoding/json.Marshal]. Replace with a faster
	// implementation (sonic, go-json, json-iterator) for better performance.
	//
	//	orm := norm.NewNorm(&norm.Config{
	//	    JSONMarshal: sonic.Marshal,
	//	})
	JSONMarshal func(v any) ([]byte, error)

	// JSONUnmarshal is the function used to unmarshal JSON into struct fields.
	// Defaults to [encoding/json.Unmarshal].
	JSONUnmarshal func(data []byte, v any) error
}

Config holds optional configuration for a Norm instance.

type Field

type Field struct {
	// contains filtered or unexported fields
}

Field holds metadata about a single struct field: its Go name, database column name, type, and parsed tag values.

func (*Field) DbName

func (f *Field) DbName() string

DbName returns the database column name (snake_case).

func (*Field) IsJSON

func (f *Field) IsJSON() bool

IsJSON reports whether the field should be marshaled/unmarshaled as JSON. A field is JSON if its underlying type is a struct (but not time.Time). Maps and slices are excluded — database drivers handle them natively.

func (*Field) JsonName

func (f *Field) JsonName() string

JsonName returns the field name in lowerCamelCase, suitable for JSON keys.

func (*Field) Name

func (f *Field) Name() string

Name returns the Go struct field name.

func (*Field) Tag

func (f *Field) Tag(tag string) (string, bool)

Tag returns the value of a norm tag key and whether it exists.

val, ok := field.Tag("default") // val="0", ok=true for `norm:"default=0"`

func (*Field) Type

func (f *Field) Type() reflect.Type

Type returns the reflect.Type of the struct field.

type Join

type Join struct {
	// contains filtered or unexported fields
}

Join is a fluent query builder for SELECT queries with JOINs. Column names are automatically prefixed with table names to avoid ambiguity. Join.Pointers collects scan targets from all joined models.

j := norm.NewJoin(mUser).
    Inner(mOrder, "orders.user_id = users.id").
    Where("users.active = ?", true).
    Limit(10)
sql, args, _ := j.Select()
err := row.Scan(j.Pointers()...)

func NewJoin

func NewJoin(base *Model) *Join

NewJoin creates a new Join builder with the given base (FROM) model.

func (*Join) Auto

func (j *Join) Auto(m *Model) *Join

Auto adds an INNER JOIN with the ON clause resolved automatically from fk struct tags. The FK relationship is searched in both directions. Panics if no FK relationship is found or if the relationship is ambiguous (multiple FKs to the same table) — use Join.Inner in those cases.

// Given: Order has `norm:"fk=User"` on UserId field
j.Auto(mOrder) // → INNER JOIN orders ON orders.user_id = users.id

func (*Join) AutoLeft

func (j *Join) AutoLeft(m *Model) *Join

AutoLeft adds a LEFT JOIN with the ON clause resolved automatically from fk struct tags. See Join.Auto for details on FK resolution.

func (*Join) Inner

func (j *Join) Inner(m *Model, on string) *Join

Inner adds an INNER JOIN with an explicit ON clause.

j.Inner(mOrder, "orders.user_id = users.id")

func (*Join) Left

func (j *Join) Left(m *Model, on string) *Join

Left adds a LEFT JOIN with an explicit ON clause.

j.Left(mOrder, "orders.user_id = users.id")

func (*Join) Limit

func (j *Join) Limit(limit int) *Join

Limit sets the LIMIT value for the query.

func (*Join) Offset

func (j *Join) Offset(offset int) *Join

Offset sets the OFFSET value for the query.

func (*Join) Order

func (j *Join) Order(orderBy string) *Join

Order sets the ORDER BY clause. Use raw SQL with table.column format.

j.Order("users.name DESC, orders.total ASC")

func (*Join) Pointers

func (j *Join) Pointers() []any

Pointers returns scan targets from all models in order (base first, then each joined model). Suitable for passing to rows.Scan().

err := row.Scan(j.Pointers()...)

func (*Join) Right

func (j *Join) Right(m *Model, on string) *Join

Right adds a RIGHT JOIN with an explicit ON clause.

j.Right(mOrder, "orders.user_id = users.id")

func (*Join) Select

func (j *Join) Select() (string, []any, error)

Select builds the full SELECT ... FROM ... JOIN ... query. All column names are prefixed with their table names. Returns the SQL string, positional arguments, and any error.

func (*Join) Where

func (j *Join) Where(where string, args ...any) *Join

Where sets the WHERE clause with "?" placeholders for positional args.

j.Where("users.active = ? AND orders.total > ?", true, 100)

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model binds cached metadata to a specific struct instance. It provides methods for building SQL queries and extracting values/pointers from the bound struct.

Model is not safe for concurrent use. Each goroutine should obtain its own Model via Norm.M.

func (Model) Binds

func (m Model) Binds(opts ...Option) string

Binds returns a comma-separated list of bind placeholders ($1, $2, ...). Supports Exclude and Fields options.

m.Binds()                    // "$1, $2, $3"
m.Binds(norm.Exclude("id")) // "$1, $2"

func (Model) BuildConditions

func (m Model) BuildConditions(conds ...Cond) ([]string, []any)

BuildConditions builds SQL WHERE conditions from typed Cond values. Returns a slice of condition strings and a slice of bind values.

Field names accept any format (struct name, camelCase, snake_case). Dot-prefixed names like "u.name" are supported — the prefix is preserved in the output SQL, and the field is looked up without it. Use Prefix to add the same prefix to all conditions at once. Use "field->>jsonKey" for JSON field access.

conds, vals := m.BuildConditions(
    norm.Eq("u.name", "John"),
    norm.Gte("u.age", 18),
    norm.In("o.id", 1, 2, 3),
    norm.IsNull("u.deleted_at", true),
)

func (*Model) Delete

func (m *Model) Delete(opts ...Option) (string, []any, error)

Delete builds a full DELETE query and returns the SQL string and WHERE args. Supports Where and Returning options.

sql, args, _ := m.Delete(norm.Where("id = ?", 42))
// "DELETE FROM users WHERE id=$1"

func (Model) FieldByName

func (m Model) FieldByName(name string) (*Field, bool)

FieldByName looks up a field by any name format (struct name, camelCase, or snake_case db name). Returns the Field and true if found.

func (Model) FieldDescriptions

func (m Model) FieldDescriptions() []*Field

FieldDescriptions returns the slice of all Field descriptors for this model.

func (Model) Fields

func (m Model) Fields(opts ...Option) string

Fields returns a comma-separated list of column names in snake_case. Supports Exclude, Fields, and Prefix options.

m.Fields()                          // "id, name, email"
m.Fields(norm.Exclude("id"))        // "name, email"
m.Fields(norm.Prefix("u."))         // "u.id, u.name, u.email"

func (*Model) Insert

func (m *Model) Insert(opts ...Option) (string, []any, error)

Insert builds a full INSERT query and returns the SQL string and values from the bound struct. Struct fields are automatically JSON-marshaled. Supports Exclude, Fields, and Returning options.

sql, vals, _ := m.Insert(norm.Exclude("id"), norm.Returning("Id"))
// "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"

func (Model) LimitOffset

func (m Model) LimitOffset(limit, offset int) string

LimitOffset returns a LIMIT/OFFSET clause string. Pass 0 to omit either part.

m.LimitOffset(10, 0)   // "LIMIT 10"
m.LimitOffset(10, 20)  // "LIMIT 10 OFFSET 20"

func (Model) NewInstance

func (m Model) NewInstance() any

NewInstance creates and returns a new zero-value pointer to the struct type that this model represents.

inst := m.NewInstance() // returns *User (zero value)

func (Model) OrderBy

func (m Model) OrderBy(orderBy string) string

OrderBy validates and renders an ORDER BY clause string. Field names are validated against the model and converted to database column names. Accepts any name format (struct name, camelCase, snake_case). Direction defaults to ASC if omitted. Panics if a field is not found or direction is invalid — these are programmer errors.

m.OrderBy("Name DESC")          // "name DESC"
m.OrderBy("Name ASC, Email")    // "name ASC, email ASC"

func (Model) Parse

func (m Model) Parse(obj any, table string) error

Parse extracts field metadata from obj and stores it in the modelMeta. obj must be a struct or pointer to struct. If table is empty, the table name is derived from the struct name in snake_case.

func (*Model) Pointer

func (m *Model) Pointer(name string) any

Pointer returns a pointer to a single named field of the bound struct. Panics if the field name is not found — this is a programmer error.

err := row.Scan(m.Pointer("Id"))

func (*Model) Pointers

func (m *Model) Pointers(opts ...Option) []any

Pointers returns a slice of pointers to the bound struct's fields, suitable for passing to rows.Scan(). Struct fields (except time.Time) are wrapped in a JSON scanner automatically. Supports Exclude, Fields, and AddTargets options.

err := row.Scan(m.Pointers()...)
err := row.Scan(m.Pointers(norm.AddTargets(&totalCount))...)

func (Model) Returning

func (m Model) Returning(fields string) string

Returning validates field names and returns a RETURNING clause string (e.g. "RETURNING id, name"). Fields is a comma-separated list of field names in any format (struct name, camelCase, or db name). Returns empty string if fields is empty. Panics if a field is not found — this is a programmer error.

m.Returning("Id")          // "RETURNING id"
m.Returning("Id, Email")   // "RETURNING id, email"

func (*Model) Select

func (m *Model) Select(opts ...Option) (string, []any, error)

Select builds a full SELECT query from the bound model. Returns the SQL string, positional arguments, and any error. Supports Exclude, Fields, Prefix, Where, Order, Limit, Offset options.

sql, args, _ := m.Select(
    norm.Where("active = ?", true),
    norm.Order("Name DESC"),
    norm.Limit(10),
)
// "SELECT id, name, email FROM users WHERE active=$1 ORDER BY name DESC LIMIT 10"

func (Model) Table

func (m Model) Table() string

Table returns the database table name for this model.

func (*Model) Update

func (m *Model) Update(opts ...Option) (string, []any, error)

Update builds a full UPDATE query and returns the SQL string and combined args (SET values followed by WHERE args). Struct fields are automatically JSON-marshaled. Bind numbering is chained: SET uses $1..$N, WHERE continues from $N+1. Supports Exclude, Fields, Where, and Returning options.

sql, args, _ := m.Update(norm.Exclude("id"), norm.Where("id = ?", user.Id))
// "UPDATE users SET name=$1, email=$2 WHERE id=$3"

func (Model) UpdateFields

func (m Model) UpdateFields(opts ...Option) (string, int)

UpdateFields returns a SET clause string ("name=$1, email=$2") and the next bind parameter number. Supports Exclude and Fields options.

set, nextBind := m.UpdateFields(norm.Exclude("id"))
// set = "name=$1, email=$2", nextBind = 3

func (*Model) Values

func (m *Model) Values(opts ...Option) []any

Values returns a slice of field values from the bound struct instance. Struct fields (except time.Time) are marshaled to JSON bytes automatically. Supports Exclude and Fields options.

_, err := pool.Exec(ctx, sql, m.Values(norm.Exclude("id"))...)

type Norm

type Norm struct {
	// contains filtered or unexported fields
}

Norm is the entry point for the norm library. It caches struct metadata so that reflection only happens once per type. Create one instance per application and reuse it — it is safe for concurrent use.

func NewNorm

func NewNorm(config *Config) *Norm

NewNorm creates a new Norm instance. Pass nil for default configuration.

orm := norm.NewNorm(nil)
orm := norm.NewNorm(&norm.Config{DefaultString: "varchar"})

func (*Norm) AddModel

func (n *Norm) AddModel(obj any, table string) *Model

AddModel registers a struct with an explicit table name and returns a Model bound to obj. Panics if obj is not a struct or pointer to struct.

orm.AddModel(&User{}, "app_users")

func (*Norm) FieldsByTable

func (n *Norm) FieldsByTable(table string) []*Field

FieldsByTable returns field descriptors for a registered table. Returns nil if the table is not registered.

func (*Norm) GetConfig

func (n *Norm) GetConfig() *Config

GetConfig returns the Norm configuration.

func (*Norm) M

func (n *Norm) M(obj any) (*Model, error)

M returns a Model bound to obj. obj must be a pointer to a struct.

Struct metadata is cached by type — reflection happens only on the first call for each struct type. Each call returns a new Model bound to the given obj instance.

user := User{Id: 1, Name: "John"}
m, err := orm.M(&user)

func (*Norm) Tables

func (n *Norm) Tables() []string

Tables returns a list of all registered table names.

type Option

type Option interface {
	Type() OptionType
	Value() any
}

Option is a functional option for customizing query building methods. Use the constructor functions (Exclude, Fields, Where, etc.) to create options.

func AddTargets

func AddTargets(targets ...any) Option

AddTargets creates an option that appends extra scan targets to the pointers returned by Model.Pointers. Useful for scanning computed columns not present in the struct.

var count int
ptrs := m.Pointers(norm.AddTargets(&count))

func Exclude

func Exclude(fields string) Option

Exclude creates an option that excludes the named fields (comma-separated db column names) from the result.

m.Fields(norm.Exclude("id,password"))

func Fields

func Fields(fields string) Option

Fields creates an option that includes only the named fields (comma-separated db column names).

m.Fields(norm.Fields("name,email"))

func Limit

func Limit(limit int) Option

Limit creates an option that adds a LIMIT clause to a SELECT query.

m.Select(norm.Limit(10))

func Offset

func Offset(offset int) Option

Offset creates an option that adds an OFFSET clause to a SELECT query.

m.Select(norm.Offset(20))

func Order

func Order(orderBy string) Option

Order creates an option that adds an ORDER BY clause to a SELECT query. Field names are validated against the model and converted to column names.

m.Select(norm.Order("Name DESC"))

func Returning

func Returning(fields string) Option

Returning creates an option that adds a RETURNING clause with the given field names (comma-separated, any name format).

m.Insert(norm.Returning("Id"))

func Where

func Where(where string, args ...any) Option

Where creates an option that adds a WHERE clause. Use "?" as placeholders for positional arguments — they are replaced with $1, $2, etc. at build time.

m.Select(norm.Where("name = ? AND age > ?", "John", 18))

type OptionType

type OptionType int

OptionType identifies the kind of an Option.

const (
	ExcludeOption    OptionType = iota // Exclude fields by db name
	FieldsOption                       // Include only specified fields
	ReturningOption                    // RETURNING clause fields
	PrefixOption                       // Table alias prefix for field names
	WhereOption                        // WHERE clause with ? placeholders
	AddTargetsOption                   // Extra scan targets for Pointers
	OffsetOption                       // OFFSET value
	LimitOption                        // LIMIT value
	OrderByOption                      // ORDER BY clause
)

Directories

Path Synopsis
Package gen generates Go struct source code from PostgreSQL database schemas.
Package gen generates Go struct source code from PostgreSQL database schemas.
Package migrate creates and alters PostgreSQL tables to match registered norm models.
Package migrate creates and alters PostgreSQL tables to match registered norm models.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL