Help us shape the future of Go! Take the 2026 Go Developer Survey

sql

package module
v1.4.9 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 22 Imported by: 3

README

Storage SQL Module

The storage/sql package is a high-performance SQL toolkit for Go built on zero-runtime-reflection and clean domain model principles.

1. Architecture and Drivers

The package supports multiple databases using the optimal driver for each:

  • PostgreSQL: implemented via the native pgx/v5 driver for maximum performance and support for PostgreSQL-specific types (JSONB, arrays).
  • MySQL, SQLite: implemented via the standard database/sql interface.

The driver is selected when creating the Db instance:

// PostgreSQL (pgx)
db, _ := sql.NewDb("postgres", dsn)

// MySQL (database/sql)
db, _ := sql.NewDb("mysql", dsn)

2. SQL Builder

The builder constructs SQL queries programmatically, providing injection safety and multi-dialect support.

Inspection and Drivers

Build() accepts a driver instance and returns the final query string with placeholders.

import "gitlab.com/gorib/sql/driver"

q := sql.Select().From(sql.Table("users")).Where(criteria.And().Add("id", "eq", 10))

// PostgreSQL: SELECT * FROM users WHERE id = $1
sqlPostgres, params, _ := q.Build(driver.NewPostgres())

// MySQL: SELECT * FROM users WHERE id = ?
sqlMysql, params, _ := q.Build(driver.NewMysql())
SELECT
// Simple select with conditions and ordering
q := sql.Select("id", "name", "email").
    From(sql.Table("users")).
    Where(criteria.And().
        Add("status", "eq", "active").
        Add("age", "gt", 18),
    ).
    Sort("created_at desc").
    Page(0, 20)

// Aliases and JOINs
q := sql.Select("u.name", "o.total").
    From(sql.Table("users").As("u"),
        sql.Join(sql.Table("orders").As("o"), "u.id = o.user_id"),
        sql.LeftJoin(sql.Table("profiles").As("p"), "u.id = p.user_id"))

// Subquery in WHERE
sub := sql.Select("id").From(sql.Table("banned_users"))
q := sql.Select().From(sql.Table("posts")).
    Where(criteria.And().Add("user_id", "in", sub))

// Column-to-column comparison
q := sql.Select().From(sql.Table("users")).
    Where(criteria.And().Add("updated_at", "gt", sql.Expr("created_at")))
INSERT, UPDATE, DELETE
// INSERT with RETURNING
q := sql.Insert("name").Into(sql.Table("users")).Values("Alice").Returning("id")

// UPDATE with aliases
q := sql.Update(map[string]any{"u.status": "active"}).
    Table(sql.Table("users").As("u")).
    From(sql.Join(sql.Table("orders").As("o"), "u.id = o.user_id")).
    Where(criteria.And().Add("o.total", "gt", 1000))

// Atomic increment via sql.Expr
q := sql.Update(map[string]any{"version": sql.Expr("version + 1")}).
    Table(sql.Table("items")).Where(criteria.And().Add("id", "eq", 1))

// DELETE
q := sql.Delete().From(sql.Table("logs")).Where(criteria.And().Add("created_at", "lt", oldDate))
PLAIN (Raw SQL)

The Plain builder executes a hand-written SQL string.

q := sql.Plain("SELECT * FROM users WHERE status = $? AND age > $?", "active", 25)

Placeholder behavior:

  1. Universal placeholder $? (recommended)

    • Portability: write the query once; the library replaces $? with the driver-native placeholder ($1, $2 for PostgreSQL, ? for MySQL).
    • Safety: the library verifies that the number of $? placeholders matches the number of arguments. A mismatch returns sql.ErrArgumentsMismatch.
  2. Native placeholders ($1, :name, ?)

    • Use these when you need database-specific features.
    • The library passes the string and arguments through as-is without validation. Mismatch errors come from the database itself.
  3. Important restriction: $? and native placeholders cannot be mixed in the same query. If at least one $? is found, the library enters auto-numbering mode, which will conflict with any existing native indices.


3. store Utility (Code Generator)

The generator creates the mapping between the database and your Go types, eliminating runtime reflection.

Defining a Stored Struct
//go:generate go tool store $GOFILE
package postgres

type storedUser struct {
    _ *model.User

    // readonly: excluded from the base Updates(),
    // but INCLUDED in a named group when that group is requested explicitly.
    Id        int       `db:"id" store:",readonly,updates:Identity"`

    Email     string    `db:"email"`
    LastLogin time.Time `db:"last_login" store:",updates:Status"`
    // The column is a nullable text; the model field is a []string.
    Roles     *string   `db:"roles" store:"Roles,convert:csv"`

    // Override the source field:
    // the Name column in the DB is populated from the FullName field of the domain model.
    Name      string    `db:"name" store:"FullName"`
}
Generated Code (simplified)
// newStoredUser — maps a domain model to the DB struct
func newStoredUser(m *model.User) *storedUser {
    return &storedUser{
        Id:    m.Id,
        Email: m.Email,
        Name:  m.FullName, // uses the overridden field name
        Roles: sql.JoinCsv(m.Roles),
    }
}

// Unwrap — maps the DB struct back to the domain model
func (m *storedUser) Unwrap() *model.User {
    return &model.User{
        Id:       m.Id,
        Email:    m.Email,
        FullName: m.Name, // mapped back to FullName
        Roles:    sql.SplitCsv(m.Roles),
    }
}

// Scan returns a slice of pointers to fields for rows.Scan()
func (m *storedUser) Scan() []any {
    return []any{&m.Id, &m.Email, &m.LastLogin, &m.Roles, &m.Name}
}

// Returns lists all column names
func (m *storedUser) Returns() []string {
    return []string{"id", "email", "last_login", "roles", "name"}
}

// Updates contains only non-readonly fields
func (m *storedUser) Updates() map[string]any {
    return map[string]any{
        "email":      m.Email,
        "last_login": m.LastLogin,
        "roles":      m.Roles,
        "name":       m.Name,
    }
}

// UpdatesIdentity ignores readonly and includes the field because the group is named explicitly
func (m *storedUser) UpdatesIdentity() map[string]any {
    return map[string]any{"id": m.Id}
}
Flags on the _ field
Flag Effect
nonew Do not generate newStoredUser.
nounwrap Do not generate Unwrap().

Use these when you need a custom mapping implementation.

Field Tags
Tag Description
db:"column" Column name in the database.
store:"SourceField" Name of the corresponding field in the domain model (when it differs from the stored struct field name).
store:",readonly" Excluded from the base Inserts() and Updates().
store:",updates:Group1|Group2" Included in the named update methods. Overrides readonly.
store:",inserts:Group1|Group2" Included in the named insert methods. Overrides readonly.
store:",convert:csv" Maps a model []string to a stored *string through sql.JoinCsv / sql.SplitCsv. Lossy: an element containing a comma comes back as two, and no elements is NULL.

4. Repository: CRUD Automation

sql.Repository[Model] is a high-level generic abstraction that automatically uses the Scannable and Returnable interfaces.

Model Requirements
Interface Method Purpose
Scannable Scan() []any Returns pointers to fields for rows.Scan.
Returnable Returns() []string Defines the column list for SELECT / RETURNING.

The store code generator produces both methods automatically.

Automatic RETURNING

The repository automatically appends Returning(...) to INSERT and UPDATE queries when the driver supports it (e.g., PostgreSQL). This lets you retrieve generated values (such as auto-incremented IDs) in the same round-trip.

func (r *userRepo) Create(ctx context.Context, user *model.User) (*model.User, error) {
    fields, values := newStoredUser(user).Inserts()

    // Repository adds .Returning() automatically based on stored.Returns()
    stored, err := r.Repository.Get(ctx, sql.Insert(fields...).Into(sql.Table("users")).Values(values...))
    if err != nil {
        return nil, err
    }

    return stored.Unwrap(), nil
}
Available Methods
Method Description
Get(ctx, builder) Returns a single *Model.
Select(ctx, builder) Returns []*Model.
Delete(ctx, builder) Executes a DELETE.
Count(ctx, builder) Returns the row count.

5. Transactions and Savepoints

Transactions are propagated via context.Context. Nested Begin calls automatically create SAVEPOINTs.

ctx, commit, rollback, err := repo.Begin(ctx)
if err != nil {
    return err
}
defer rollback()

if err := repo.Save(ctx, user); err != nil {
    return err
}

return commit()

6. Additional Features

JSON Fields

Use sql.Json[T] for JSON / JSONB columns. The wrapper handles marshalling and unmarshalling transparently.

type storedPayment struct {
    Meta *sql.Json[model.PaymentMeta] `db:"meta"`
}

7. Telemetry

NewDb returns the pool wrapped in a tracing decorator, so every operation produces a CLIENT span and a db.client.operation.duration observation without the caller doing anything.

Attribute On Values
db.system.name span and metric postgresql, mysql, sqlite
db.operation.name span and metric begin, query, queryRow, exec, commit, rollback
error.type metric, on failure conflict, deadlock, no_rows, bad_request, internal, connect, tx
db.query.text span only the statement

The split in that last row is the point: operation names are a closed set and make a safe metric label, a query string is not. error.type comes from the same classification as sql.NewError, so a metric and a handled error agree on what went wrong.

Durations are in seconds with explicit second-scale buckets — the SDK default is millisecond-scaled and would put every query in one bucket.

The provider is read from the context per operation rather than captured at construction, so a query inherits the trace of the request that issued it. The instruments themselves are cached per provider by hilton.NewInstruments instead of being rebuilt on every call.

A transaction is one span: Begin opens it, Commit or Rollback closes it, and the statements inside are children. A transaction that is never finished is closed by a runtime.AddCleanup finalizer rather than leaking the span.

waffle registers the pool under config/service/sql, as di.Fallback() — a service that needs different options declares its own.


Installing the store Utility

Register it as a module tool, so the version is pinned in go.mod alongside the other generators:

go get -tool gitlab.com/gorib/sql/cmd/store

It is then invoked as go tool store — which is what the //go:generate line in a stored struct file expects:

//go:generate go tool store $GOFILE

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrCannotConnectDb = errors.New("unable to connect to a db")

	ErrNoTransactionInContext = errors.New("no started transaction in context")
	ErrCannotStartTx          = errors.New("unable to start a transaction")

	ErrCannotCreateSavepoint   = errors.New("unable to create a savepoint")
	ErrCannotReleaseSavepoint  = errors.New("unable to release a savepoint")
	ErrCannotRollbackSavepoint = errors.New("unable to rollback a savepoint")

	ErrCannotCommitTransaction   = errors.New("unable to commit a transaction")
	ErrCannotRollbackTransaction = errors.New("unable to rollback a transaction")

	ErrExprBuilderNotImplemented = errors.New("expression builder is not defined")
	ErrUnsupportedQueryMode      = errors.New("unsupported query mode")

	ErrArgumentsMismatch = errors.New("number of placeholders does not match number of arguments")
)
View Source
var (
	ErrQueryFailed = errors.New("query failed")
	ErrConflict    = errors.New("duplicate key value violates unique constraint")
	ErrNoRows      = errors.New("no rows in result set")
	ErrBadRequest  = errors.New("bad request")
	ErrDeadlock    = errors.New("deadlock")
	ErrInternal    = errors.New("internal error")
)
View Source
var Expr = Plain

Functions

func Delete

func Delete() *deleteQuery

func FromError

func FromError(err error) (*e, bool)

func Insert

func Insert(fields ...string) *insertQuery

func Join

func Join(f fromMain, condition string) fromOptions

func JoinCsv

func JoinCsv(v []string) *string

No elements is NULL rather than an empty string, so an absent column reads back as absent.

func LeftJoin

func LeftJoin(f fromMain, condition string) fromOptions

func NewContextWithTransaction

func NewContextWithTransaction(ctx context.Context, connection Db) (_ context.Context, commit func() error, rollback func() error, _ error)

func NewError

func NewError(err error) error

func NewRepository

func NewRepository[Model any, _ ScannableModel[Model]](connection Db, opts ...repositoryOption) (*repository[Model], error)

func NewStorage

func NewStorage[Model any, scan ScannableModel[Model]](connection Db, from *from, opts ...repositoryOption) (*storage[Model], error)

func Plain

func Plain(query string, params ...any) *plainBuilder

func Query

func Query(q SelectQuery, alias string) fromQuery

func RightJoin

func RightJoin(f fromMain, condition string) fromOptions

func Select

func Select(fields ...string) *selectQuery

func SplitCsv

func SplitCsv(v *string) []string

An empty string is no elements: strings.Split would answer one empty element and put a value in the model that nobody stored. A NOT NULL column defaulting to the empty string is the usual way in.

func Table

func Table(table string) fromTable

func Update

func Update(values map[string]any) *updateQuery

func WithExtraMapping

func WithExtraMapping(mapping map[string]string) repositoryOption

Types

type BuildOption

type BuildOption func(*builderCfg) error

func WithMappings

func WithMappings(mappings map[string]string) BuildOption

type Builder

type Builder interface {
	Build(driver Driver, opts ...BuildOption) (string, []any, error)
}

type CountBuilder

type CountBuilder interface {
	Builder
	// contains filtered or unexported methods
}

type CountQuery

type CountQuery interface {
	CountBuilder
	From(main fromMain, opts ...fromOptions) CountQuery
	Where(query criteria.Expression) CountQuery
	Returning(fields ...string) CountQuery
}

func Count

func Count() CountQuery

type Db

type Db interface {
	Connect(ctx context.Context) (Db, error)
	Begin(ctx context.Context) (Tx, error)
	Driver() Driver
	DbSql() (*dbSql.DB, error)
}

func NewDb

func NewDb(driver, dsn string) (Db, error)

type DeleteBuilder

type DeleteBuilder interface {
	Builder
	// contains filtered or unexported methods
}

type DeleteQuery

type DeleteQuery interface {
	DeleteBuilder
	From(main fromTable, opts ...fromOptions) DeleteQuery
	Where(query criteria.Expression) DeleteQuery
}

type Driver

type Driver interface {
	Field(field string, operation string, value any, params *[]any) (string, *[]any, error)
	Returning() bool
	Placeholder(pos int) string
	Conflict(conflict string, updates []string) string
	UpdateAsJoin() bool
}

type Field

type Field string

type InsertQuery

type InsertQuery interface {
	Builder
	Into(table fromTable) InsertQuery
	Values(values ...any) InsertQuery
	From(main fromMain, opts ...fromOptions) InsertQuery
	Conflict(conflict string, excluded ...string) InsertQuery
	Returning(fields ...string) InsertQuery
}

type Json

type Json[T any] struct {
	// contains filtered or unexported fields
}

func NewJson

func NewJson[T any](value T) *Json[T]

func (*Json[T]) MarshalJSON

func (f *Json[T]) MarshalJSON() ([]byte, error)

func (*Json[T]) Scan

func (f *Json[T]) Scan(value any) error

func (*Json[T]) ScanText

func (j *Json[T]) ScanText(v pgtype.Text) error

func (Json[T]) TextValue

func (j Json[T]) TextValue() (pgtype.Text, error)

func (*Json[T]) UnmarshalJSON

func (f *Json[T]) UnmarshalJSON(value []byte) error

func (*Json[T]) Unwrap

func (f *Json[T]) Unwrap() T

func (*Json[T]) Value

func (f *Json[T]) Value() (driver.Value, error)

type Mappable

type Mappable interface {
	Mappings() map[string]string
}

type Repository

type Repository[Model any] interface {
	Transactional
	Get(ctx context.Context, builder Builder) (*Model, error)
	Select(ctx context.Context, builder Builder) ([]*Model, error)
	Delete(ctx context.Context, builder DeleteBuilder) error
	Count(ctx context.Context, query CountBuilder) (int, error)
}

type Result

type Result interface {
	RowsAffected() int64
}

type Returnable

type Returnable interface {
	Returns() []string
}

type Row

type Row interface {
	Scan(dest ...any) error
}

type Rows

type Rows interface {
	Close()
	Err() error
	Next() bool
	Scan(dest ...any) error
}

type Scannable

type Scannable interface {
	Scan() []any
}

type ScannableModel

type ScannableModel[Model any] interface {
	*Model
	Scannable
	Returnable
}

type SelectQuery

type SelectQuery interface {
	Builder
	From(main fromMain, opts ...fromOptions) SelectQuery
	Where(query criteria.Expression) SelectQuery
	Page(page int, count int) SelectQuery
	Sort(sort ...string) SelectQuery
	Window(window string) SelectQuery
	Returning(fields ...string) SelectQuery
	Group(fields ...string) SelectQuery
}

type Storage

type Storage[Model any] interface {
	View[Model]
	Create(ctx context.Context, model *Model) (*Model, error)
	Update(ctx context.Context, model *Model) (*Model, error)

	Remove(ctx context.Context, expr criteria.Expression) error
}

type Transactional

type Transactional interface {
	Begin(ctx context.Context) (childCtx context.Context, commit func() error, rollback func() error, err error)
}

type Tx

type Tx interface {
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) Row
	Exec(ctx context.Context, query string, args ...any) (Result, error)
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

func TransactionFromContext

func TransactionFromContext(ctx context.Context) (Tx, error)

type UpdateQuery

type UpdateQuery interface {
	Builder
	Table(table fromTable) UpdateQuery
	From(opts ...fromOptions) UpdateQuery
	Where(query criteria.Expression) UpdateQuery
	Returning(fields ...string) UpdateQuery
}

type Value

type Value any

type Valueable

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

func Between

func Between(field string, from, to any) Valueable

func Eq

func Eq(field string, value any) Valueable

func Ge

func Ge(field string, value any) Valueable

func Gt

func Gt(field string, value any) Valueable

func ILike

func ILike(field string, value any) Valueable

func JsonEq

func JsonEq(field string, value any) Valueable

func JsonIn

func JsonIn(field string, value any) Valueable

func Le

func Le(field string, value any) Valueable

func Like

func Like(field string, value any) Valueable

func Lt

func Lt(field string, value any) Valueable

func Ne

func Ne(field string, value any) Valueable

type Valuer

type Valuer interface {
	Value() (Value, error)
}

type View

type View[Model any] interface {
	Repository() Repository[Model]
	Get(ctx context.Context, expr criteria.Expression) (*Model, error)
	Find(ctx context.Context, expr criteria.Expression) ([]*Model, error)
	Count(ctx context.Context, expr criteria.Expression) (int, error)
}

Directories

Path Synopsis
cmd
store command

Jump to

Keyboard shortcuts

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