sqlutil

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: MPL-2.0 Imports: 6 Imported by: 0

README

sqlutil

Package sqliteutil provides utility functions to work with the standard database/sql package in Go. Its main purpose is to format SQL queries and map columns to struct fields.

Getting started

Installation

go get codeberg.org/phipu/sqlutil

Define a struct with db struct tags.

type Car struct {
	ID    int64  `db:"id"`
	Make  string `db:"make"`
	Model string `db:"model"`
}

Use reflection to extract meta data and store it in a variable. From the meta data we can extract the columns. This can be done globally.

var (
	carMeta = sqlutil.NewMeta[Car]()
	cols    = carMeta.Cols(nil)
)
Select

With this setup, we can start to query the DB. In the following examples we use SQLite. To build a query, we use the Builder. It provides a fmt.Printf style interface. We can use the normal database/sql interface to execute the query.

func selectCarByID1(id int64, tx *sql.Tx) (Car, error) {
	b := sqlutil.Builder{Dialect: sqlutil.SQLite}
	b.Printf("SELECT %s FROM car WHERE id = %v", cols.Names(), id)

	row := tx.QueryRow(b.String(), b.Args()...)

	var res Car
	err := row.Scan(cols.BindPtr(&res).Vals(0)...)
	return res, err
}

This works well, but we have to repeat the columns we want to select. First we have to specify the column names in the query, and then we have to specify the column values in the call to scan. This works for one argument, but can be tedious, especially in a join. To avoid this, we can use bound calls in the Printf call and the builder will remember the Vals(0) in its internal ScanArgs. ScanBoundRow scans into ScanArgs.

func selectCarByID2(id int64, tx *sql.Tx) (Car, error) {
	b := sqlutil.Builder{Dialect: sqlutil.SQLite}
	var c Car
	b.Printf("SELECT %s FROM car WHERE id = %v", cols.BindPtr(&c), id)
	err := b.ScanBoundRow(tx)
	return c, err
}
Insert

Inserting can be done the same way as selecting. But assume we want the DB to decide the ID. If we use cols.Names() this includes the id. The solution is to use the argument to Cols to filter out the ID.

var insertCols = carMeta.Cols(func(name string) bool { return name != "id" })

We can then use this to insert a new car without specifying the ID. On the Builder there is a helper that will call Exec followed by GetLastInsertId called ExecLastInsertID.

func insertCar(c Car, tx *sql.Tx) (int64, error) {
	b := sqlutil.Builder{Dialect: sqlutil.SQLite}
	b.Printf("INSERT INTO car (%s) VALUES (%s)", insertCols.Names(), insertCols.Bind(c).Vals(0))
	id, err := b.ExecLastInsertID(tx)
	return id, err
}
Update

To update a row, there is the Assign wrapper. It formats bound columns to "col = ?". Note the use of b.Exec() which is just a shortcut for tx.Exec(b.String(), b.Args()...).

var updateCols = carMeta.Cols(func(name string) bool { return name != "id" })

func updateCar(c Car, tx *sql.Tx) error {
	b := sqlutil.Builder{Dialect: sqlutil.SQLite}
	b.Printf("UPDATE car SET %v WHERE id = %v", updateCols.Bind(c).Assign(), c.ID)
	_, err := b.Exec(tx)
	return err
}
Join

The builder can be used to generate a SQL query joining multiple tables. For this example, we have 3 Tables, make, model and car. The example function joins make and model and maps everything to Car.

type dbCar struct {
	ID      int64 `db:"id"`
	MakeID  int64 `db:"make_id"`
	ModelID int64 `db:"model_id"`
}

type dbMake struct {
	ID   int64  `db:"id"`
	Name string `db:"name"`
}

type dbModel struct {
	ID   int64  `db:"id"`
	Name string `db:"name"`
}

var (
	dbCarCols   = sqlutil.NewMeta[dbCar]().Cols(nil)
	dbMakeCols  = sqlutil.NewMeta[dbMake]().Cols(nil)
	dbModelCols = sqlutil.NewMeta[dbModel]().Cols(nil)
)

type Car struct {
	ID      int64
	MakeID  int64
	Make    string
	ModelID int64
	Model   string
}

func getCar(id int64, tx *sql.Tx) (Car, error) {
	b := sqlutil.Builder{Dialect: sqlutil.SQLite}

	var (
		c  dbCar
		ma dbMake
		mo dbModel
	)

	b.Printf("SELECT %s, %s, %s FROM car AS c ",
		dbCarCols.Map(sqlutil.MapPrefix("c.")).BindPtr(&c),
		dbMakeCols.Map(sqlutil.MapPrefix("ma.")).BindPtr(&ma),
		dbModelCols.Map(sqlutil.MapPrefix("mo.")).BindPtr(&mo),
	)
	b.Printf("JOIN make AS ma ON c.make_id = ma.id ")
	b.Printf("JOIN model AS mo ON c.model_id = mo.id ")
	b.Printf("WHERE c.id = %v", id)

	err := b.ScanBoundRow(tx)
	if err != nil {
		return Car{}, err
	}

	return Car{c.ID, ma.ID, ma.Name, mo.ID, mo.Name}, nil
}

Licensing

sqlutil is released under the MPL v2.0 license

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Scan added in v0.2.0

func Scan(s SQLRow, d ...any) error

func ScriptMigration

func ScriptMigration(script string) func(*sql.Tx) error

Types

type Assign added in v0.3.0

type Assign struct{ C BoundCols }

Assign is formatted as Col = Val by Printf

func (Assign) WriteSQL added in v0.3.0

func (a Assign) WriteSQL(w *RawWriter)

WriteSQL implements SQLWriter for Assign

type BoundCols added in v0.3.0

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

BoundCols is the combination of column names and corresponding values

func (BoundCols) Assign added in v0.3.0

func (c BoundCols) Assign() Assign

func (BoundCols) Iter added in v0.3.0

func (c BoundCols) Iter() iter.Seq2[Raw, any]

func (BoundCols) Len added in v0.3.0

func (c BoundCols) Len() int

Len returns the number of rows, that is, the exclusive maximum that can be passed to Vals

func (BoundCols) Names added in v0.3.0

func (c BoundCols) Names() Names

func (BoundCols) String added in v0.3.0

func (c BoundCols) String() string

func (BoundCols) Vals added in v0.3.0

func (c BoundCols) Vals(idx int) Vals

type Builder

type Builder struct {
	Dialect Dialect
	// contains filtered or unexported fields
}

Builder is used to build an SQL query

func (*Builder) Args

func (b *Builder) Args() []any

func (*Builder) DeleteWhere added in v0.3.0

func (b *Builder) DeleteWhere(table Raw, whereFormat string, a ...any)

func (*Builder) Exec added in v0.0.4

func (b *Builder) Exec(db DB) (sql.Result, error)

func (*Builder) ExecLastInsertID added in v0.2.0

func (b *Builder) ExecLastInsertID(db DB) (int64, error)

func (*Builder) ExecRowsAffected added in v0.2.0

func (b *Builder) ExecRowsAffected(db DB) (int64, error)

func (*Builder) InsertValues added in v0.3.0

func (b *Builder) InsertValues(table Raw, a BoundCols)

InsertValues prints INSERT INTO ... VALUES (...), ...

func (*Builder) JoinBy added in v0.2.0

func (b *Builder) JoinBy(sep string)

JoinBy will cause subsequent calls to Printf to be joined by sep if sep is not empty.

func (*Builder) Printf

func (b *Builder) Printf(format string, a ...any)

Printf appends a fmt.Printf style query to b. Instead of printing the arguments directly, they will be replaced by a placeholder generated by Dialect. Raw arguments will be printed directly. If an argument implements SQLWriter, the WriteSQL method will be called (see documentation of SQLWriter). Because of this special handling, it is best to use %v as string parameter.

func (*Builder) Query added in v0.0.4

func (b *Builder) Query(db DB) (*sql.Rows, error)

func (*Builder) QueryRow added in v0.0.4

func (b *Builder) QueryRow(db DB) *sql.Row

func (*Builder) Reset added in v0.0.9

func (b *Builder) Reset()

Reset resets the Builder so that it can be re-used. Args() and ScanArgs() slices are re-used.

func (*Builder) Scan added in v0.2.0

func (b *Builder) Scan(db DB, d ...any) Rows

func (*Builder) ScanArgs added in v0.2.0

func (b *Builder) ScanArgs() []any

func (*Builder) ScanBound added in v0.3.0

func (b *Builder) ScanBound(db DB) Rows

func (*Builder) ScanBoundRow added in v0.3.0

func (b *Builder) ScanBoundRow(db DB) error

func (*Builder) ScanRow added in v0.0.4

func (b *Builder) ScanRow(db DB, d ...any) error

func (*Builder) SelectWhere added in v0.3.0

func (b *Builder) SelectWhere(c BoundCols, table Raw, whereFormat string, a ...any)

SelectWhere prints SELECT ... FROM ...

func (*Builder) String

func (b *Builder) String() string

func (*Builder) UpdateWhere added in v0.3.0

func (b *Builder) UpdateWhere(table Raw, c BoundCols, whereFormat string, a ...any)

UpdateWhere prints UPDATE ... SET ... WHERE ...

type ColsFor added in v0.3.0

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

ColsFor is a subset of all struct fields of struct type T. It is safe to use ColsFor from multiple goroutines.

func (ColsFor[T]) Bind added in v0.3.0

func (c ColsFor[T]) Bind(v ...T) BoundCols

func (ColsFor[T]) BindPtr added in v0.3.0

func (c ColsFor[T]) BindPtr(p ...*T) BoundCols

func (ColsFor[T]) Map added in v0.3.0

func (c ColsFor[T]) Map(mapFunc ColsMapFunc) ColsFor[T]

Map returns c but with mapFunc applied to each name

func (ColsFor[T]) Names added in v0.3.0

func (c ColsFor[T]) Names() Names

func (ColsFor[T]) String added in v0.3.0

func (c ColsFor[T]) String() string

func (ColsFor[T]) WriteSQL added in v0.3.0

func (c ColsFor[T]) WriteSQL(w *RawWriter)

WriteSQL implements SQLWriter for ColsFor

type ColsMapFunc added in v0.3.0

type ColsMapFunc = func(name Raw) Raw

func MapPrefix added in v0.3.0

func MapPrefix(prefix Raw) ColsMapFunc

MapPrefix returns a ColsMapFunc that adds prefix to the column name

type ColsPred added in v0.3.0

type ColsPred = func(name string) bool

type DB added in v0.0.9

type DB interface {
	Exec(query string, a ...any) (sql.Result, error)
	Query(query string, a ...any) (*sql.Rows, error)
	QueryRow(query string, a ...any) *sql.Row
}

DB describes the common interface of *sql.Tx and *sql.Rows

type Dialect added in v0.0.9

type Dialect interface {
	WriteParam(sb *strings.Builder, idx int)
}

Dialect is used to customize how SQL placeholders are formatted (?, $n, @n, ...)

var (
	SQLite Dialect = sqliteDialectImpl{}
)

type Meta added in v0.2.0

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

Meta contains field information about a struct. It is safe to use Meta from multiple goroutines.

func NewMeta added in v0.2.0

func NewMeta[T any]() Meta[T]

func (Meta[T]) Cols added in v0.2.0

func (m Meta[T]) Cols(pred ColsPred) ColsFor[T]

Cols returns the columns for the struct type T where pred returns true

type Migration

type Migration struct {
	Version int64
	Migrate func(tx *sql.Tx) error
}

type Names added in v0.3.0

type Names []Raw

Names are formatted as comma seperated list of strings

func (Names) String added in v0.3.0

func (n Names) String() string

type Raw

type Raw string

Raw will be formatted as raw SQL

type RawWriter added in v0.2.0

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

func (*RawWriter) Write added in v0.2.0

func (b *RawWriter) Write(d []byte) (int, error)

Write appends the contents of d to b's buffer. Write always returns len(p), nil.

func (*RawWriter) WriteParam added in v0.2.0

func (b *RawWriter) WriteParam(v any)

WriteParam appends a param placeholder to bs buffer

type Rows added in v0.2.0

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

Rows wraps the result of Query

func (*Rows) Close added in v0.2.0

func (r *Rows) Close() error

func (*Rows) Err added in v0.2.0

func (r *Rows) Err() error

func (*Rows) Next added in v0.2.0

func (r *Rows) Next() bool

Next continues with the next row and scans into the arguments provided to ScanRow

type SQLRow added in v0.2.0

type SQLRow interface {
	Scan(d ...any) error
}

SQLRow describes the common interface of *sql.Rows and *sql.Row

type SQLWriter added in v0.2.0

type SQLWriter interface {
	WriteSQL(w *RawWriter)
}

SQLWriter is used by Builder.Printf

type Vals

type Vals []any

Vals are the value of / pointer to struct fields

func (Vals) WriteSQL added in v0.2.0

func (v Vals) WriteSQL(w *RawWriter)

WriteSQL implements SQLWriter for Vals

Directories

Path Synopsis
ex0
ex1

Jump to

Keyboard shortcuts

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