Documentation
¶
Overview ¶
Package xsql is a minimal, stdlib-style layer over database/sql that provides type-safe scanning into structs, primitives, and sql.Scanner types. You write plain SQL; xsql maps results into Go values with a tiny, predictable API.
Overview ¶
xsql preserves database/sql semantics while removing repetitive row-mapping code. It works with *sql.DB, *sql.Tx, and *sql.Conn. Mapping is deterministic, fast, and easy to reason about in code review.
Mapping rules ¶
- Fields bind by `db:"name"` first; otherwise case-insensitive field ←→ column name.
- Nested structs can be flattened with `db:",inline"`.
- If a destination type (or field) implements sql.Scanner, its Scan method receives the driver value.
- Primitives (bool, numbers, string, []byte, time.Time, sql.RawBytes) are supported directly.
- Extra columns are ignored; missing columns yield zero values (favors robustness).
Performance ¶
On first use of a (Type, ColumnSet) pair, xsql builds a scan plan (column → field index path and destination strategy). Plans and per-type indexes are cached in a lazily-initialized, concurrency-safe map (sync.Map). Subsequent scans reuse the plan and avoid reflection on the hot path. Common safe conversions (e.g., []byte→string, numeric widenings) are handled inline.
Error handling ¶
- Get returns sql.ErrNoRows when no row matches.
- Query and Exec propagate underlying driver errors.
- Iterator / protocol issues surface via rows.Err() at the end of Query.
Compatibility ¶
xsql works with any database/sql driver (PostgreSQL, MySQL, SQLite, SQL Server, Oracle). It does not rewrite SQL or placeholders; write queries exactly as your driver expects.
Usage notes ¶
Prefer explicit column lists over SELECT * to keep mapping stable. Add LIMIT 1 (or the equivalent) when you expect a single row. Use contexts to bound query timeouts. Keep Go types close to database types to minimize surprises. For large reads, consider streaming with Query and processing incrementally if memory usage matters.
xsql is intended for production systems that value clarity and performance over abstraction. It keeps the API small and predictable while giving you full control over your SQL.
named.go
Index ¶
- Variables
- func Exec(ctx context.Context, e Execer, query string, args ...any) (sql.Result, error)
- func Get[T any](ctx context.Context, q Querier, query string, args ...any) (out T, err error)
- func NamedExec(ctx context.Context, e Execer, ph Placeholder, query string, params ...any) (sql.Result, error)
- func NamedQuery[T any](ctx context.Context, q Querier, ph Placeholder, query string, params ...any) ([]T, error)
- func Query[T any](ctx context.Context, q Querier, query string, args ...any) (out []T, err error)
- func Rebind(query string, ph Placeholder, params ...any) (string, []any, error)
- type Beginner
- type Execer
- type Mapper
- type Placeholder
- type Querier
Constants ¶
This section is empty.
Variables ¶
var ErrDuplicateKeyTag = errors.New("xsql: named bind: duplicate key from struct tags/fields")
ErrDuplicateKeyTag is returned when two struct fields (including embedded) resolve to the same logical parameter name (case-insensitive), e.g. via db:"name".
var ErrNilParams = errors.New("xsql: named bind: nil params")
ErrNilParams is returned when named binding is requested with a nil pointer or nil params value. This typically means you passed a nil *struct to Rebind.
var ErrUnsupportedArg = errors.New("xsql: named bind: params must be struct or map[string]any")
ErrUnsupportedArg is returned when the single named-binding argument is not a struct or map[string]any (e.g., passing an int or map[int]any).
Functions ¶
func Exec ¶
Exec executes a statement that does not return rows (INSERT, UPDATE, DELETE, DDL).
It forwards to the underlying Execer. On success it returns the driver's sql.Result, which may support LastInsertId and RowsAffected depending on the database/driver.
Exec does not attempt SQL rendering or placeholder rewriting; write your SQL exactly as your driver expects.
Example:
// Given a *sql.DB (or *sql.Tx, *sql.Conn) in variable `db`: ctx := context.Background() res, err := xsql.Exec(ctx, db, `INSERT INTO users (email) VALUES (?)`, "[email protected]") if err != nil { log.Fatal(err) } n, _ := res.RowsAffected() fmt.Println("rows:", n)
Notes:
- Use a transaction (BeginTx) around multiple Exec/Query calls when you need atomicity.
- Not all drivers support LastInsertId; prefer RETURNING with Query/Get where available.
func Get ¶
Get executes the SQL query and scans the first row into a value of type T.
It returns sql.ErrNoRows if the query yields no rows and does not enforce "exactly one row" beyond the first; if more rows exist, they are ignored. You should use LIMIT 1 (or an equivalent WHERE clause) when you require at-most-one row.
T may be a struct (supports `db` tags and ,inline), a primitive, or any type implementing sql.Scanner. Column mapping prefers `db:"name"` tags; otherwise it matches case-insensitive field names.
Extra columns are ignored and missing columns set zero values unless strict mode is enabled internally. Safe for concurrent use, Get internally uses a lazily-initialized, concurrency-safe plan cache based on sync.Map, which avoids global locks for most read operations.
Example:
// Given a *sql.DB (or *sql.Tx, *sql.Conn) in variable `db`:
type User struct {
ID int64 `db:"id"`
Email string `db:"email"`
}
ctx := context.Background()
u, err := xsql.Get[User](ctx, db, `SELECT id, email FROM users WHERE id = $1`, 42)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// handle not found
} else {
// handle other errors
}
}
// use u
func NamedExec ¶ added in v0.1.1
func NamedExec(ctx context.Context, e Execer, ph Placeholder, query string, params ...any) (sql.Result, error)
NamedExec is a convenience for Exec with named or positional arguments. It calls Rebind, then ExecContext on your Execer (e.g., *sql.DB, *sql.Tx, *sql.Conn).
Example:
_, err := xsql.NamedExec(ctx, db, xsql.PlaceholderAtP,
`UPDATE items SET price=:p WHERE id IN (:ids)`,
map[string]any{"p": 100, "ids": []int{7,8,9}},
)
func NamedQuery ¶ added in v0.1.1
func NamedQuery[T any](ctx context.Context, q Querier, ph Placeholder, query string, params ...any) ([]T, error)
NamedQuery runs a query with named or positional arguments and scans results using your existing Query[T]. Use this when you want []T back with minimal ceremony.
Example:
type User struct { ID int64 `db:"id"`; Email string `db:"email"` }
rows, err := xsql.NamedQuery[User](ctx, db, xsql.PlaceholderDollar,
`SELECT id, email FROM users WHERE status=:s`,
map[string]any{"s":"active"},
)
func Query ¶
Query executes the SQL query and scans all result rows into a slice of T.
T may be a struct (supports `db` tags and ,inline), a primitive, or any type implementing sql.Scanner. Column mapping prefers `db:"name"` tags; otherwise it matches case-insensitive field names.
Extra columns are ignored and missing columns set zero values unless strict mode is enabled internally. Safe for concurrent use, Query internally uses a lazily-initialized, concurrency-safe plan cache based on sync.Map, which avoids global locks for most read operations.
Example:
// Given a *sql.DB (or *sql.Tx, *sql.Conn) in variable `db`:
type User struct {
ID int64 `db:"id"`
Email string `db:"email"`
}
ctx := context.Background()
users, err := xsql.Query[User](ctx, db, `SELECT id, email FROM users ORDER BY id`)
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Println(u.ID, u.Email)
}
func Rebind ¶ added in v0.1.1
Rebind resolves :named parameters (if applicable) and rewrites placeholders.
Usage:
Named style (exactly one struct or map[string]any): sql, args, err := xsql.Rebind( `SELECT * FROM users WHERE status=:status AND id IN (:ids)`, xsql.PlaceholderDollar, map[string]any{"status":"active", "ids":[]int{1,2,3}}, ) // sql => SELECT * FROM users WHERE status=$1 AND id IN ($2,$3,$4) // args => ["active", 1, 2, 3]
Notes: slices/arrays expand; []byte is scalar; empty slice/array becomes NULL (so `IN (NULL)` matches no rows on most engines).
Positional passthrough (any other params shape): // params are already positional; only placeholder rewriting is applied sql, args, _ := xsql.Rebind(`a=? AND b=?`, xsql.PlaceholderColonNum, "A", 10)
Rules of thumb:
- Pass exactly one struct or map to use :named binding.
- Pass multiple values (or a non-struct/map) to use positional args.
- SQL scanning safely skips quoted strings, comments, and PostgreSQL $tag$…$tag$ blocks.
Types ¶
type Execer ¶
type Execer interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
Execer is implemented by *sql.DB, *sql.Tx, *sql.Conn, and any wrapper that can execute a statement that does not return rows.
type Mapper ¶
type Mapper struct {
Strict bool // reserved: future strict mode (not enforced here)
// contains filtered or unexported fields
}
Mapper owns caches. Use the package-level lazy getter (getMapper) or create your own in tests.
type Placeholder ¶ added in v0.1.1
type Placeholder int
Placeholder selects the positional parameter style for a target database.
Common choices:
- PlaceholderQuestion → "?" (MySQL, SQLite, DuckDB, ClickHouse)
- PlaceholderDollar → "$1, $2, …" (PostgreSQL)
- PlaceholderAtP → "@p1, @p2…" (SQL Server)
- PlaceholderColonNum → ":1, :2, …" (Oracle)
const ( PlaceholderQuestion Placeholder = iota PlaceholderDollar PlaceholderAtP PlaceholderColonNum )
func PlaceholderFor ¶ added in v0.1.1
func PlaceholderFor(driverName string) Placeholder
PlaceholderFor picks a Placeholder based on a driver name string. This is a convenience for one-off calls; you can also choose the enum directly.
Examples:
ph := xsql.PlaceholderFor("pgx") // => PlaceholderDollar
ph := xsql.PlaceholderFor("sqlserver") // => PlaceholderAtP
ph := xsql.PlaceholderFor("mysql") // => PlaceholderQuestion