sqlh

package module
v0.0.0-...-3ae4af0 Latest Latest
Warning

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

Go to latest
Published: Jan 21, 2025 License: MIT Imports: 12 Imported by: 0

README

Build Status GoDoc Go Report Card

sqlh

A lightweight Go database helper library.

Attempts to make SQL easier in Go without adding all the weight of an ORM.

Problems this library is trying to solve

I want to be able to use slice parameters for IN clauses!
names, err := sqlh.Pluck[string](
  sqlh.SQL(`SELECT name FROM users WHERE id IN (?)`, sqlh.In(userIDs)).Query(db),
)
// SELECT name FROM users WHERE id IN (1, 2, 3)
I want to be able to use SQL subqueries as parameters!
subquery := sqlh.SQL(`SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?`, 10)
query := sqlh.SQL(`SELECT event FROM events WHERE user_id IN (?)`, subquery)
// SELECT event FROM events WHERE user_id IN (SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?), 10
I want to be able to use slices as multi INSERT values!
query := sqlh.SQL(`INSERT INTO a (id, name) VALUES ?`, sqlh.Values([]any{1, "hello"}, []any{2, "test"}))
// "INSERT INTO a (id, name) VALUES (?, ?), (?, ?)", [1, "hello", 2, "test"]
I want to scan SQL values directly into protobuf structs!
rows, err := sqlh.SQL(`SELECT name, address FROM users WHERE id IN (?)`, sqlh.In(userIDs)).Query(db)
users, err := sqlh.Scan[proto.UserResponse](rows, func (v *proto.UserResponse, row sqlh.Row) error {
	return row.Scan(&v.UserName, &v.Address)
})
I want to scan JSON values directly into their go counterparts!
var out map[string]string
err := sqlh.SQL(`SELECT json_data FROM store`).Scan(sqlh.Json(&out))
// {"hello": "test"}
I want to scan JSON values as parameters!
rows, err := sqlh.SQL(`SELECT json_data FROM store WHERE json_overlaps(json_data, ?)`, sqlh.Json(map[string]string {"hello": "test"})).Query(db)
// {"hello": "test"}
Ok, but surely this isn't going to scale to a big codebase? I want to see where query fragments are coming from!
subquery := sqlh.DebugSQL(`SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?`, 10)
query := sqlh.DebugSQL(`SELECT event FROM events WHERE user_id IN (?)`, subquery)
// query is annotated with the place it was created:
// /* debug_test.go:10 */ SELECT event FROM events WHERE user_id IN (
//   /* debug_test.go:9 */ SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?
// )

Example

var SQL = sqlh.SQL

if testing.Testing() {
    SQL = sqlh.DebugSQL
}

type LatestEvent struct {
	name string
	updatedAt time.Time
}

ownerIDs := []int{1, 5, 10}

// build a subquery with an argument
grouped := SQL(`SELECT events.name, MAX(events.updated_at) AS 'updated_at'
FROM events
WHERE events.owner_id IN (?)
GROUP BY events.name`, sqlh.In(ownerIDs))

// combine the two queries
query := SQL(`SELECT grouped.name, grouped.updated_at
FROM (?) AS grouped
grouped.updated_at DESC
LIMIT ?, ?`, grouped, 10, 100)

// execute the query and get some rows back
rows, err := query.QueryContext(ctx, db)
if err != nil { return err }

// use Scan to build a slice of structs from each row
events, err := sqlh.Scan(rows, func(e *LatestEvent, scan func(dest ...any) error) error {
	return scan(&e.name, &e.updatedAt)
})
if err != nil { return err }

for _, event := range events {
	fmt.Println(event.name, event.updatedAt)
}

Documentation

Overview

Package sqlh provides lightweight sql helpers.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Binary

func Binary(v interface {
	encoding.BinaryMarshaler
	encoding.BinaryUnmarshaler
}) interface {
	sql.Scanner
	driver.Valuer
}

Binary converts to or from a binary value.

Example
var location url.URL
_ = db.QueryRow("SELECT location FROM binary_example").Scan(sqlh.Binary(&location))
fmt.Println(location.String())
Output:

http://example.com

func Iter

func Iter(rows Rows, fn func() error) (err error)

Iter calls fn for each successful call to rows.Next, closing the rows at the end.

func Json

func Json(v any) interface {
	sql.Scanner
	driver.Valuer
}

Json converts to or from a json value.

Example
var document any
_ = db.QueryRow("SELECT document FROM json_example").Scan(sqlh.Json(&document))
fmt.Println(document)
Output:

[1 2 3]

func Pluck

func Pluck[V any](rows Rows, queryErr error) (out []V, err error)

Pluck will scan the results of a query that produces a single column.

Example
userIDs := []int{1, 2, 3}
names, _ := sqlh.Pluck[string](sqlh.SQL(`SELECT name FROM users WHERE id IN (?)`, sqlh.In(userIDs)).Query(db))
for _, name := range names {
	fmt.Println(name)
}
Output:

user a
user b

func Scan

func Scan[V any](rows Rows, scan func(*V, Row) error) (out []*V, err error)

Scan takes a function that can scan a given sql.Rows into []*V.

func ScanV

func ScanV[V any](rows Rows, scan func(*V, Row) error) (out []V, err error)

ScanV takes a function that can scan a given sql.Rows into []V.

func Text

func Text(v interface {
	encoding.TextMarshaler
	encoding.TextUnmarshaler
}) interface {
	sql.Scanner
	driver.Valuer
}

Text converts to or from a binary value.

Types

type Expr

type Expr struct {
	Statement string
	Args      []any
}

func DebugSQL

func DebugSQL(stmt string, args ...any) Expr

DebugSQL annotates the query with the caller and indents it if it contains a newline.

Example
package main

import (
	"fmt"
	"github.com/simon-engledew/sqlh"
)

func main() {
	subquery := sqlh.DebugSQL(`SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?`, 10)
	query := sqlh.DebugSQL(`SELECT event FROM events WHERE user_id IN (?)`, subquery)
	fmt.Println(query.Statement)
}
Output:

/* debug_test.go:12 */ SELECT event FROM events WHERE user_id IN (
	/* debug_test.go:11 */ SELECT id FROM users WHERE suspended_at IS NULL AND parent_id = ?
)

func In

func In[T any, S ~[]T](items S) Expr

In takes parameters and returns an Expr that can be used in an SQL IN clause.

Example
package main

import (
	"fmt"
	"github.com/simon-engledew/sqlh"
)

func main() {
	query := sqlh.SQL(`SELECT name FROM in_example WHERE id IN (?)`, sqlh.In([]int{1, 2, 3}))
	fmt.Println(query.Statement, query.Args)
}
Output:

SELECT name FROM in_example WHERE id IN (?, ?, ?) [1 2 3]

func SQL

func SQL(stmt string, args ...any) Expr

SQL takes an SQL fragment and returns an Expr that flattens any nested queries and their arguments.

Example
clause := sqlh.SQL("found = ?", true)
expr := sqlh.SQL(`SELECT name FROM builder_example WHERE id = ? AND ?`, 1, clause)
var name string
_ = db.QueryRow(expr.Statement, expr.Args...).Scan(&name)
fmt.Println(name)
Output:

example

func Values

func Values(values ...[]any) Expr

Values allows you to build a multi-row insert statement.

func (Expr) Exec

func (e Expr) Exec(db interface {
	Exec(query string, args ...any) (sql.Result, error)
}) (sql.Result, error)

Exec calls db.Exec, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.Exec

func (Expr) ExecContext

func (e Expr) ExecContext(ctx context.Context, db interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}) (sql.Result, error)

ExecContext calls db.ExecContext, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.ExecContext

func (Expr) Query

func (e Expr) Query(db interface {
	Query(query string, args ...any) (*sql.Rows, error)
}) (*sql.Rows, error)

Query calls db.Query, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.Query

func (Expr) QueryContext

func (e Expr) QueryContext(ctx context.Context, db interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}) (*sql.Rows, error)

QueryContext calls db.QueryContext, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.QueryContext

func (Expr) QueryRow

func (e Expr) QueryRow(db interface {
	QueryRow(query string, args ...any) *sql.Row
}) *sql.Row

QueryRow calls db.QueryRow, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.QueryRow

func (Expr) QueryRowContext

func (e Expr) QueryRowContext(ctx context.Context, db interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}) *sql.Row

QueryRowContext calls db.QueryRowContext, passing in the SQL statement and its arguments. See https://pkg.go.dev/database/sql#DB.QueryRowContext

func (Expr) String

func (e Expr) String() string

type Row

type Row interface {
	Scan(...any) error
	ColumnTypes() ([]*sql.ColumnType, error)
}

type Rows

type Rows interface {
	Row
	Close() error
	Next() bool
	Err() error
}

Rows allows wrappers for sql.Rows to be passed to the scanning functions.

Jump to

Keyboard shortcuts

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