A powerful and flexible collection of Go helpers for database operations. sqlutil simplifies common database tasks with a clean, chainable API that works with PostgreSQL, MySQL, and SQLite.
- Clean, intuitive API for CRUD operations
- Support for multiple SQL flavors (PostgreSQL, MySQL, SQLite)
- Flexible filtering with comparison operators (=, <>, >, <, >=, <=, LIKE, IN, NOT IN)
- Pagination and ordering support
- Field selection to retrieve only needed columns
- Row locking support (FOR UPDATE)
- Works with
*sql.DB,*sql.Conn, and*sql.Tx - Built on top of sqlquery and scany
go get github.com/allisson/sqlutil- Quick Start
- Database Setup
- Basic CRUD Operations
- Advanced Queries
- Bulk Operations
- Using with Transactions
- Multiple SQL Flavors
package main
import (
"context"
"database/sql"
"fmt"
"log"
"github.com/allisson/sqlutil"
_ "github.com/lib/pq"
)
type User struct {
ID int `db:"id"`
Name string `db:"name" fieldtag:"insert,update"`
Email string `db:"email" fieldtag:"insert,update"`
Age int `db:"age" fieldtag:"insert,update"`
}
func main() {
// Connect to database
db, err := sql.Open("postgres", "postgres://user:password@localhost/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
flavor := sqlutil.PostgreSQLFlavor
// Insert a user
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
if err := sqlutil.Insert(ctx, db, flavor, "insert", "users", &user); err != nil {
log.Fatal(err)
}
// Get a user
var alice User
opts := sqlutil.NewFindOptions(flavor).WithFilter("email", "alice@example.com")
if err := sqlutil.Get(ctx, db, "users", opts, &alice); err != nil {
log.Fatal(err)
}
fmt.Printf("Found user: %+v\n", alice)
// Update the user
alice.Age = 31
if err := sqlutil.Update(ctx, db, flavor, "update", "users", alice.ID, &alice); err != nil {
log.Fatal(err)
}
// Delete the user
if err := sqlutil.Delete(ctx, db, flavor, "users", alice.ID); err != nil {
log.Fatal(err)
}
}For the examples below, we'll use this table structure:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER NOT NULL,
country VARCHAR(100),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Run a PostgreSQL database with Docker:
docker run --name sqlutil-postgres \
-e POSTGRES_USER=user \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=sqlutil \
-p 5432:5432 \
-d postgres:14-alpineInsert a single record into the database:
package main
import (
"context"
"database/sql"
"log"
"github.com/allisson/sqlutil"
_ "github.com/lib/pq"
)
type User struct {
ID int `db:"id"`
Name string `db:"name" fieldtag:"insert,update"`
Email string `db:"email" fieldtag:"insert,update"`
Age int `db:"age" fieldtag:"insert,update"`
Country string `db:"country" fieldtag:"insert,update"`
Active bool `db:"active" fieldtag:"insert,update"`
}
func main() {
db, _ := sql.Open("postgres", "postgres://user:password@localhost/sqlutil?sslmode=disable")
defer db.Close()
ctx := context.Background()
flavor := sqlutil.PostgreSQLFlavor
// Insert a new user
newUser := User{
Name: "Bob Smith",
Email: "bob@example.com",
Age: 28,
Country: "USA",
Active: true,
}
// The "insert" tag tells sqlutil to use fields marked with fieldtag:"insert"
if err := sqlutil.Insert(ctx, db, flavor, "insert", "users", &newUser); err != nil {
log.Fatal(err)
}
log.Println("User inserted successfully")
}Retrieve a single record from the database:
// Get user by ID
var user User
opts := sqlutil.NewFindOptions(flavor).WithFilter("id", 1)
if err := sqlutil.Get(ctx, db, "users", opts, &user); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
// Get user by email
var userByEmail User
opts = sqlutil.NewFindOptions(flavor).WithFilter("email", "bob@example.com")
if err := sqlutil.Get(ctx, db, "users", opts, &userByEmail); err != nil {
log.Fatal(err)
}
// Get user with specific fields only
var partialUser struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
opts = sqlutil.NewFindOptions(flavor).
WithFields([]string{"id", "name", "email"}).
WithFilter("id", 1)
if err := sqlutil.Get(ctx, db, "users", opts, &partialUser); err != nil {
log.Fatal(err)
}Retrieve multiple records from the database:
// Get all users
var users []User
opts := sqlutil.NewFindAllOptions(flavor)
if err := sqlutil.Select(ctx, db, "users", opts, &users); err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d users\n", len(users))
// Get active users only
var activeUsers []User
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("active", true)
if err := sqlutil.Select(ctx, db, "users", opts, &activeUsers); err != nil {
log.Fatal(err)
}
// Get users with pagination
var pagedUsers []User
opts = sqlutil.NewFindAllOptions(flavor).
WithLimit(10).
WithOffset(0).
WithOrderBy("created_at DESC")
if err := sqlutil.Select(ctx, db, "users", opts, &pagedUsers); err != nil {
log.Fatal(err)
}Update existing records:
// Update user by ID
user := User{
ID: 1,
Name: "Bob Smith Jr.",
Email: "bob.jr@example.com",
Age: 29,
Country: "USA",
Active: true,
}
// The "update" tag tells sqlutil to use fields marked with fieldtag:"update"
if err := sqlutil.Update(ctx, db, flavor, "update", "users", user.ID, &user); err != nil {
log.Fatal(err)
}
// Update with custom options (more flexible)
updateOpts := sqlutil.NewUpdateOptions(flavor).
WithSet("age", 30).
WithSet("country", "Canada").
WithFilter("id", 1)
if err := sqlutil.UpdateWithOptions(ctx, db, flavor, "users", updateOpts); err != nil {
log.Fatal(err)
}
// Bulk update - update all inactive users
updateOpts = sqlutil.NewUpdateOptions(flavor).
WithSet("active", false).
WithFilter("age.lt", 18)
if err := sqlutil.UpdateWithOptions(ctx, db, flavor, "users", updateOpts); err != nil {
log.Fatal(err)
}Delete records from the database:
// Delete user by ID
if err := sqlutil.Delete(ctx, db, flavor, "users", 1); err != nil {
log.Fatal(err)
}
// Delete with custom options (more flexible)
deleteOpts := sqlutil.NewDeleteOptions(flavor).WithFilter("active", false)
if err := sqlutil.DeleteWithOptions(ctx, db, flavor, "users", deleteOpts); err != nil {
log.Fatal(err)
}
// Delete users older than 65
deleteOpts = sqlutil.NewDeleteOptions(flavor).WithFilter("age.gt", 65)
if err := sqlutil.DeleteWithOptions(ctx, db, flavor, "users", deleteOpts); err != nil {
log.Fatal(err)
}
// Delete users from specific country
deleteOpts = sqlutil.NewDeleteOptions(flavor).WithFilter("country", "USA")
if err := sqlutil.DeleteWithOptions(ctx, db, flavor, "users", deleteOpts); err != nil {
log.Fatal(err)
}sqlutil supports a wide range of filter operators:
ctx := context.Background()
flavor := sqlutil.PostgreSQLFlavor
// Equality
opts := sqlutil.NewFindAllOptions(flavor).WithFilter("age", 30)
// WHERE age = 30
// Not equal
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("age.not", 30)
// WHERE age <> 30
// Greater than
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("age.gt", 18)
// WHERE age > 18
// Greater than or equal
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("age.gte", 18)
// WHERE age >= 18
// Less than
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("age.lt", 65)
// WHERE age < 65
// Less than or equal
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("age.lte", 65)
// WHERE age <= 65
// LIKE operator
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("name.like", "%Smith%")
// WHERE name LIKE '%Smith%'
// IN operator
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("country.in", "USA,Canada,Mexico")
// WHERE country IN ('USA', 'Canada', 'Mexico')
// NOT IN operator
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("country.notin", "USA,Canada")
// WHERE country NOT IN ('USA', 'Canada')
// IS NULL
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("country.null", true)
// WHERE country IS NULL
// IS NOT NULL
opts = sqlutil.NewFindAllOptions(flavor).WithFilter("country.null", false)
// WHERE country IS NOT NULL
// Multiple filters (AND condition)
opts = sqlutil.NewFindAllOptions(flavor).
WithFilter("age.gte", 18).
WithFilter("age.lte", 65).
WithFilter("active", true).
WithFilter("country", "USA")
// WHERE age >= 18 AND age <= 65 AND active = true AND country = 'USA'
// Complex example: Find active users aged 25-40 from specific countries
var users []User
opts = sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithFilter("age.gte", 25).
WithFilter("age.lte", 40).
WithFilter("country.in", "USA,Canada,UK")
if err := sqlutil.Select(ctx, db, "users", opts, &users); err != nil {
log.Fatal(err)
}Retrieve only the fields you need:
// Define a struct with only the fields you need
type UserBasic struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
// Select only specific fields
var users []UserBasic
opts := sqlutil.NewFindAllOptions(flavor).
WithFields([]string{"id", "name", "email"}).
WithFilter("active", true)
if err := sqlutil.Select(ctx, db, "users", opts, &users); err != nil {
log.Fatal(err)
}
// SELECT id, name, email FROM users WHERE active = true
// Get single user with limited fields
var userBasic UserBasic
opts := sqlutil.NewFindOptions(flavor).
WithFields([]string{"id", "name", "email"}).
WithFilter("id", 1)
if err := sqlutil.Get(ctx, db, "users", opts, &userBasic); err != nil {
log.Fatal(err)
}Implement pagination for large result sets:
// Page 1: First 10 users
var page1 []User
opts := sqlutil.NewFindAllOptions(flavor).
WithLimit(10).
WithOffset(0).
WithOrderBy("id ASC")
if err := sqlutil.Select(ctx, db, "users", opts, &page1); err != nil {
log.Fatal(err)
}
// Page 2: Next 10 users
var page2 []User
opts = sqlutil.NewFindAllOptions(flavor).
WithLimit(10).
WithOffset(10).
WithOrderBy("id ASC")
if err := sqlutil.Select(ctx, db, "users", opts, &page2); err != nil {
log.Fatal(err)
}
// Pagination helper function
func GetUserPage(ctx context.Context, db *sql.DB, page, pageSize int) ([]User, error) {
var users []User
offset := (page - 1) * pageSize
opts := sqlutil.NewFindAllOptions(flavor).
WithLimit(pageSize).
WithOffset(offset).
WithOrderBy("id ASC")
err := sqlutil.Select(ctx, db, "users", opts, &users)
return users, err
}
// Usage
page1Users, err := GetUserPage(ctx, db, 1, 20) // First page, 20 items
page2Users, err := GetUserPage(ctx, db, 2, 20) // Second page, 20 itemsSort results by one or multiple columns:
// Order by single column ascending
opts := sqlutil.NewFindAllOptions(flavor).WithOrderBy("name ASC")
// Order by single column descending
opts = sqlutil.NewFindAllOptions(flavor).WithOrderBy("created_at DESC")
// Order by multiple columns
opts = sqlutil.NewFindAllOptions(flavor).WithOrderBy("country ASC, age DESC")
// Complex example: Get top 10 youngest active users from USA
var youngUsers []User
opts = sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithFilter("country", "USA").
WithOrderBy("age ASC").
WithLimit(10)
if err := sqlutil.Select(ctx, db, "users", opts, &youngUsers); err != nil {
log.Fatal(err)
}Use row locking for concurrent operations:
// FOR UPDATE - locks rows for update
opts := sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithForUpdate("")
// FOR UPDATE SKIP LOCKED - skip rows that are already locked
opts = sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithForUpdate("SKIP LOCKED")
// FOR UPDATE NOWAIT - return immediately if rows are locked
opts = sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithForUpdate("NOWAIT")
// Example: Process queue items with row locking
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
var queueItems []QueueItem
opts := sqlutil.NewFindAllOptions(flavor).
WithFilter("status", "pending").
WithOrderBy("created_at ASC").
WithLimit(10).
WithForUpdate("SKIP LOCKED")
if err := sqlutil.Select(ctx, tx, "queue", opts, &queueItems); err != nil {
log.Fatal(err)
}
// Process items...
for _, item := range queueItems {
// Process item
updateOpts := sqlutil.NewUpdateOptions(flavor).
WithSet("status", "processed").
WithFilter("id", item.ID)
if err := sqlutil.UpdateWithOptions(ctx, tx, flavor, "queue", updateOpts); err != nil {
log.Fatal(err)
}
}
tx.Commit()Efficiently handle multiple operations:
// Insert multiple users
users := []User{
{Name: "Alice", Email: "alice@example.com", Age: 30, Country: "USA"},
{Name: "Bob", Email: "bob@example.com", Age: 25, Country: "Canada"},
{Name: "Charlie", Email: "charlie@example.com", Age: 35, Country: "UK"},
}
for _, user := range users {
if err := sqlutil.Insert(ctx, db, flavor, "insert", "users", &user); err != nil {
log.Fatal(err)
}
}
// Bulk update with filter
updateOpts := sqlutil.NewUpdateOptions(flavor).
WithSet("active", false).
WithFilter("country.in", "USA,Canada,Mexico")
if err := sqlutil.UpdateWithOptions(ctx, db, flavor, "users", updateOpts); err != nil {
log.Fatal(err)
}
// Bulk delete with filter
deleteOpts := sqlutil.NewDeleteOptions(flavor).
WithFilter("active", false).
WithFilter("age.lt", 18)
if err := sqlutil.DeleteWithOptions(ctx, db, flavor, "users", deleteOpts); err != nil {
log.Fatal(err)
}sqlutil works seamlessly with database transactions:
// Start a transaction
tx, err := db.BeginTx(ctx, nil)
if err != nil {
log.Fatal(err)
}
defer tx.Rollback() // Rollback if not committed
// Insert user
user := User{Name: "David", Email: "david@example.com", Age: 40, Country: "France"}
if err := sqlutil.Insert(ctx, tx, flavor, "insert", "users", &user); err != nil {
log.Fatal(err)
}
// Get the inserted user
var insertedUser User
opts := sqlutil.NewFindOptions(flavor).WithFilter("email", "david@example.com")
if err := sqlutil.Get(ctx, tx, "users", opts, &insertedUser); err != nil {
log.Fatal(err)
}
// Update the user
insertedUser.Age = 41
if err := sqlutil.Update(ctx, tx, flavor, "update", "users", insertedUser.ID, &insertedUser); err != nil {
log.Fatal(err)
}
// Commit the transaction
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
// Example: Transfer operation with rollback
func TransferUser(ctx context.Context, db *sql.DB, userID int, newCountry string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// Get user
var user User
opts := sqlutil.NewFindOptions(flavor).WithFilter("id", userID)
if err := sqlutil.Get(ctx, tx, "users", opts, &user); err != nil {
return err
}
// Update country
updateOpts := sqlutil.NewUpdateOptions(flavor).
WithSet("country", newCountry).
WithFilter("id", userID)
if err := sqlutil.UpdateWithOptions(ctx, tx, flavor, "users", updateOpts); err != nil {
return err
}
// Commit transaction
return tx.Commit()
}sqlutil supports PostgreSQL, MySQL, and SQLite:
import (
"github.com/allisson/sqlutil"
_ "github.com/lib/pq"
)
db, _ := sql.Open("postgres", "postgres://user:password@localhost/mydb?sslmode=disable")
flavor := sqlutil.PostgreSQLFlavor
opts := sqlutil.NewFindOptions(flavor).WithFilter("id", 1)import (
"github.com/allisson/sqlutil"
_ "github.com/go-sql-driver/mysql"
)
db, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/mydb")
flavor := sqlutil.MySQLFlavor
opts := sqlutil.NewFindOptions(flavor).WithFilter("id", 1)import (
"github.com/allisson/sqlutil"
_ "github.com/mattn/go-sqlite3"
)
db, _ := sql.Open("sqlite3", "./mydb.db")
flavor := sqlutil.SQLiteFlavor
opts := sqlutil.NewFindOptions(flavor).WithFilter("id", 1)Write database-agnostic code by passing the flavor as a parameter:
type UserRepository struct {
db *sql.DB
flavor sqlutil.Flavor
}
func NewUserRepository(db *sql.DB, flavor sqlutil.Flavor) *UserRepository {
return &UserRepository{db: db, flavor: flavor}
}
func (r *UserRepository) GetByID(ctx context.Context, id int) (*User, error) {
var user User
opts := sqlutil.NewFindOptions(r.flavor).WithFilter("id", id)
err := sqlutil.Get(ctx, r.db, "users", opts, &user)
return &user, err
}
func (r *UserRepository) GetActiveUsers(ctx context.Context) ([]User, error) {
var users []User
opts := sqlutil.NewFindAllOptions(r.flavor).
WithFilter("active", true).
WithOrderBy("name ASC")
err := sqlutil.Select(ctx, r.db, "users", opts, &users)
return users, err
}
// Works with any database flavor
pgRepo := NewUserRepository(pgDB, sqlutil.PostgreSQLFlavor)
mysqlRepo := NewUserRepository(mysqlDB, sqlutil.MySQLFlavor)
sqliteRepo := NewUserRepository(sqliteDB, sqlutil.SQLiteFlavor)Here's a complete example demonstrating various features:
package main
import (
"context"
"database/sql"
"fmt"
"log"
"github.com/allisson/sqlutil"
_ "github.com/lib/pq"
)
type User struct {
ID int `db:"id"`
Name string `db:"name" fieldtag:"insert,update"`
Email string `db:"email" fieldtag:"insert,update"`
Age int `db:"age" fieldtag:"insert,update"`
Country string `db:"country" fieldtag:"insert,update"`
Active bool `db:"active" fieldtag:"insert,update"`
}
func main() {
// Connect to database
db, err := sql.Open("postgres", "postgres://user:password@localhost/sqlutil?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
flavor := sqlutil.PostgreSQLFlavor
// Create table
_, err = db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
age INTEGER NOT NULL,
country VARCHAR(100),
active BOOLEAN DEFAULT true
)
`)
if err != nil {
log.Fatal(err)
}
// Insert users
users := []User{
{Name: "Alice Johnson", Email: "alice@example.com", Age: 30, Country: "USA", Active: true},
{Name: "Bob Smith", Email: "bob@example.com", Age: 25, Country: "Canada", Active: true},
{Name: "Charlie Brown", Email: "charlie@example.com", Age: 35, Country: "UK", Active: true},
{Name: "David Lee", Email: "david@example.com", Age: 28, Country: "USA", Active: false},
}
for _, user := range users {
if err := sqlutil.Insert(ctx, db, flavor, "insert", "users", &user); err != nil {
log.Printf("Error inserting user: %v", err)
}
}
// Get single user by email
var alice User
opts := sqlutil.NewFindOptions(flavor).WithFilter("email", "alice@example.com")
if err := sqlutil.Get(ctx, db, "users", opts, &alice); err != nil {
log.Fatal(err)
}
fmt.Printf("Found user: %+v\n", alice)
// Get all active users from USA
var activeUSAUsers []User
opts2 := sqlutil.NewFindAllOptions(flavor).
WithFilter("active", true).
WithFilter("country", "USA").
WithOrderBy("name ASC")
if err := sqlutil.Select(ctx, db, "users", opts2, &activeUSAUsers); err != nil {
log.Fatal(err)
}
fmt.Printf("Active USA users: %d\n", len(activeUSAUsers))
// Get users with age between 25 and 30
var youngUsers []User
opts3 := sqlutil.NewFindAllOptions(flavor).
WithFilter("age.gte", 25).
WithFilter("age.lte", 30).
WithOrderBy("age ASC")
if err := sqlutil.Select(ctx, db, "users", opts3, &youngUsers); err != nil {
log.Fatal(err)
}
fmt.Printf("Users aged 25-30: %d\n", len(youngUsers))
// Update user
alice.Age = 31
alice.Country = "Canada"
if err := sqlutil.Update(ctx, db, flavor, "update", "users", alice.ID, &alice); err != nil {
log.Fatal(err)
}
// Bulk update: deactivate all users older than 30
updateOpts := sqlutil.NewUpdateOptions(flavor).
WithSet("active", false).
WithFilter("age.gt", 30)
if err := sqlutil.UpdateWithOptions(ctx, db, flavor, "users", updateOpts); err != nil {
log.Fatal(err)
}
// Get users with pagination
page1 := []User{}
paginationOpts := sqlutil.NewFindAllOptions(flavor).
WithLimit(2).
WithOffset(0).
WithOrderBy("name ASC")
if err := sqlutil.Select(ctx, db, "users", paginationOpts, &page1); err != nil {
log.Fatal(err)
}
fmt.Printf("Page 1: %d users\n", len(page1))
// Delete inactive users
deleteOpts := sqlutil.NewDeleteOptions(flavor).WithFilter("active", false)
if err := sqlutil.DeleteWithOptions(ctx, db, flavor, "users", deleteOpts); err != nil {
log.Fatal(err)
}
fmt.Println("Inactive users deleted")
}This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.