Lightweight SQL toolkit for Go on top of database/sql:
- query builder (non-ORM)
- PostgreSQL + SQLite support
- named parameters
- model CRUD
- transactions, savepoints, upsert, returning
Production-oriented, pre-v1 API (v0.x recommended).
- Go:
1.22+ - Module path:
github.com/pafthang/dbx
- Core DB wrapper:
Open,MustOpen,NewFromDB,WithContextBegin,BeginTx,Transactional,TransactionalContextSavepoint,RollbackTo,Release
- Query API:
NewQuery,Bind,ExecuteAll,One,Row,Rows,Column,Pluck,Map
- Select builder:
Select,From, joins,Where,GroupBy,Having,OrderByLimit,Offset,Page,PerPageUnion,UnionAll- CTE:
With,WithRecursive,WithRaw - analytics helpers:
Count,CountDistinct,Exists
- DML:
Insert,InsertMany,Update,Delete,UpsertInsertReturning,InsertManyReturning,UpdateReturning,DeleteReturningUpsertOnConflictviaOnConflictDSL
- Model CRUD:
Model(...).Insert/Update/Delete/Save- PK tag support (
db:"id,pk") - auto PK fill on insert
- Operational tooling:
- query/exec hooks (
QueryLogFunc,ExecLogFunc) - trace hook (
TraceHook) - optional prepared statement cache (
EnableStatementCache)
- query/exec hooks (
- Generic helpers:
SelectAll[T],SelectOne[T],SelectOneOrZero[T]QueryAll[T],QueryOne[T]
go get github.com/pafthang/dbxSQLite (CGO-free):
import _ "modernc.org/sqlite"PostgreSQL:
import _ "github.com/jackc/pgx/v5/stdlib"db, err := dbx.Open("sqlite", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()var names []string
err := db.NewQuery("SELECT name FROM users WHERE status={:status}").
Bind(dbx.Params{"status": "active"}).
Column(&names)type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
var users []User
err := db.Select("id", "name", "email").
From("users").
Where(dbx.And(
dbx.HashExp{"status": "active"},
dbx.Like("name", "al"),
)).
OrderBy("id DESC").
Page(1, 20).
All(&users)type User struct {
ID int64 `db:"id,pk"`
Name string `db:"name"`
Email string `db:"email"`
}
func (User) TableName() string { return "users" }
u := &User{Name: "alice", Email: "a@example.com"}
_ = db.Model(u).Insert()
_ = db.Model(u).Update("name")
_ = db.Model(u).Delete()Supported join methods:
JoinInnerJoinLeftJoinRightJoinFullJoinCrossJoinJoinSubqueryLeftJoinSubqueryInnerJoinSubquery
sub := db.Select("user_id").From("events").Where(dbx.HashExp{"kind": "login"})
err := db.Select("u.id", "u.name").
From("users u").
LeftJoin("profiles p", dbx.Raw("p.user_id = u.id")).
JoinSubquery(sub, "e", dbx.Raw("e.user_id = u.id")).
Where(dbx.HashExp{"u.status": "active"}).
All(&users)active := db.Select("id", "name").
From("users").
Where(dbx.HashExp{"status": "active"})
err := db.Select("id", "name").
With("active_users", active).
From("active_users").
All(&users)total, _ := db.Select("id").From("users").Count()
distinctNames, _ := db.Select("name").From("users").CountDistinct("name")
exists, _ := db.Select("id").From("users").Where(dbx.HashExp{"email": "a@example.com"}).Exists()var names []string
_ = db.Select("name").From("users").Pluck(&names)
statusByName := map[string]string{}
_ = db.Select("name", "status").From("users").Map(&statusByName)_ = db.Upsert("users", dbx.Params{
"email": "a@example.com",
"name": "alice-updated",
}, "email").Execute()_ = db.UpsertOnConflict("users", dbx.Params{
"email": "a@example.com",
"name": "alice",
}, dbx.Conflict("email").DoUpdateColumns("name")).Execute()var id int64
var name string
_ = db.UpdateReturning("users", dbx.Params{"name": "alice-v2"}, dbx.HashExp{"id": 1}, "id", "name").
Row(&id, &name)_ = db.InsertMany("users", []dbx.Params{
{"email": "a@example.com", "name": "alice"},
{"email": "b@example.com", "name": "bob"},
}).Execute()Bulk insert with returning:
rows, _ := db.InsertManyReturning("users", []dbx.Params{
{"email": "a@example.com", "name": "alice"},
{"email": "b@example.com", "name": "bob"},
}, "id", "name").Rows()
defer rows.Close()_ = db.Transactional(func(tx *dbx.Tx) error {
if err := tx.Savepoint("sp1"); err != nil {
return err
}
if _, err := tx.Insert("users", dbx.Params{"name": "temp"}).Execute(); err != nil {
return err
}
if err := tx.RollbackTo("sp1"); err != nil {
return err
}
return tx.Release("sp1")
})users, _ := dbx.SelectAll[User](db.Select("id", "name").From("users"))
user, _ := dbx.SelectOne[User](db.Select("id", "name").From("users").Where(dbx.HashExp{"id": 1}))QueryLogFuncExecLogFunc
db.TraceHook = func(ctx context.Context, sql string, exec bool) (context.Context, func(error)) {
start := time.Now()
return ctx, func(err error) {
_ = time.Since(start)
_ = err
_ = sql
_ = exec
}
}db = db.EnableStatementCache(128)- Supported dialects:
- SQLite (
sqlite,sqlite3) - PostgreSQL (
postgres,pgx)
- SQLite (
- Placeholders:
- SQLite:
? - PostgreSQL:
$1,$2, ...
- SQLite:
- Quoting:
- SQLite: backticks
- PostgreSQL: double quotes
FULL JOIN:- available in builder API
- verify target DB support in your deployment
RETURNING:- supported by this package where DB supports it
Run all tests:
go test ./...Run benchmarks:
go test -run '^$' -bench . -benchmemPostgreSQL integration tests require DBX_PG_DSN.
Local PostgreSQL via Docker:
docker compose up -d postgres
export DBX_PG_DSN='postgres://dbx:dbx@localhost:55432/dbx_test?sslmode=disable'
go test ./... -run PostgresIntegrationMIT. See LICENSE.