sqlex

June 26, 2026 · View on GitHub

English | 中文

CI Go Report Card GoDoc License: MIT Mentioned in Awesome Go

sqlex

A drop-in replacement for jmoiron/sqlx — 100% API-compatible, with added Hook aspects, generic JSON types, bug fixes, and more.

sqlex is fully API-compatible with sqlx. All sqlx methods (Get, Select, Exec, NamedQuery, Preparex, etc.) work identically. Migrating takes 30 seconds — just change the import path. New features are purely additive and optional.

- import "github.com/jmoiron/sqlx"
+ import "github.com/go-sqlex/sqlex"

What you get for free after migrating:

  • 🚀 Auto-Rebind — write ? everywhere, works on PostgreSQL ($1), MySQL (?), SQLite (?), SQL Server (@p1). No more manual db.Rebind(). Including Preparex.
  • 🐛 SQL parsing fixes — colons in strings, :: type casts, ? in comments are correctly handled. Silent bugs from sqlx are gone.
  • 🎯 Unified interfacesExt / ExtContext / NamedExt / BindExt / Preparer / PreparerContext with compile-time checks. Write func f(ext NamedExt) and pass DB, Tx, or Conn.
  • 🔀 Auto IN expansion — slices in IN (?) detected and expanded automatically on all methods.
  • 🪝 Hook system — pluggable SQL interceptors for logging, tracing, metrics (onion model).
  • 📦 JSONValue[T] — generic JSON column type with auto serialize/deserialize.
  • 🛡️ StrictMode — lenient by default (matching sqlx Unsafe()), optionally strict for debugging.
  • 🛠️ 20+ bug fixes — data corruption, panics, silent data loss, and cross-database failures from jmoiron/sqlx, all fixed. See Critical Bug Fixes.

Migration Guide

Installation

go get github.com/go-sqlex/sqlex

Requires Go 1.21 or later.

Migration from jmoiron/sqlx

30 seconds, 3 steps:

1. Change import path:

// old
import "github.com/jmoiron/sqlx"

// new
import "github.com/go-sqlex/sqlex"

2. Change package references:

// old
db, err := sqlx.Connect("postgres", dsn)

// new
db, err := sqlex.Connect("postgres", dsn)

3. Update go.mod:

go get github.com/go-sqlex/sqlex

Done. All your existing sqlx code works without changes.

Note on StrictMode: sqlex defaults to lenient mode (strict=false), matching sqlx's db.Unsafe() behavior (silently ignore extra columns). You kept db.Unsafe() in your codebase? No changes needed — sqlex inherits the same lenient default. To enable strict struct-field matching for debugging, call db.SetStrict(true).

Gradual adoption

New features are optional — adopt at your own pace:

StepActionTime
1Replace import path30s
2Switch transactions to CloseWithErr patternper-use
3Use NamedGet/NamedSelect instead of NamedQuery + manual scanper-use
4Register custom Hooks (logging, tracing, metrics)as needed

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/go-sqlex/sqlex"
    _ "github.com/mattn/go-sqlite3"
)

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
}

func main() {
    // Connect to database
    db, err := sqlex.Connect("sqlite3", ":memory:")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Create table
    db.MustExec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)`)
    db.MustExec(`INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')`)
    db.MustExec(`INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com')`)

    // Query single row
    var user User
    err = db.Get(&user, "SELECT * FROM users WHERE id = ?", 1)
    fmt.Printf("User: %+v\n", user)

    // Query multiple rows
    var users []User
    err = db.Select(&users, "SELECT * FROM users")
    fmt.Printf("Users: %+v\n", users)
}

New Features

sqlex preserves all sqlx APIs and adds the following capabilities:

FeatureDescription
Hook aspectsAddHook — pluggable SQL execution interceptors (onion model)
JSONValue[T]types.JSONValue[T] — generic JSON column type
NamedGet/NamedSelectNamed parameter convenience methods on DB/Tx (built-in IN expansion)
CloseWithErrAuto Commit/Rollback based on error
Unified interfacesExt / ExtContext / NamedExt / BindExt / Preparer / PreparerContext — DB, Tx, and Conn share identical method signatures with compile-time checks
Auto IN expansionAll methods auto-detect slice args and expand IN clauses
Auto RebindAll query methods auto-convert ? to target database placeholders
StrictModeOptional strict struct-field matching for debugging (off by default)
Cross-database out of the boxWrite SQL with ? everywhere — works on PostgreSQL, MySQL, SQLite, SQL Server

Critical Bug Fixes from sqlx

sqlex fixes 20+ known bugs from jmoiron/sqlx — including data corruption, panics, and silent failures:

BugImpactsqlx Issue
Select + sql.RawBytesData corruption — driver buffer reuse across rows silently overwrites scanned data#931
In panic on nil driver.ValuerPanic — nil pointer Valuers crash instead of returning NULL#952
fixBound VALUES drops rowsSilent data loss — batch INSERT/UPDATE with VALUES (...) silently skips rows#898
NextResultSet cache staleData corruption — multi-result-set scans with different columns produce wrong data#857
Rebind replaces ? in stringsWrong SQL? inside string literals, comments, identifiers replaced with bind vars
Named query colons in stringsWrong SQL — IPv6 addresses, URLs, time formats misidentified as named parameters#947
ConnectContext connection leakResource leak — connection not closed on Ping failure
PostgreSQL :: type castWrong SQL::int misidentified as named parameter#428
Named queries fail on PostgreSQLCross-DB broken — Named methods don't Rebind, fail on $N databases
IN(?) not expanded on Exec/QueryxRuntime error — slice args not expanded on some methods
Unified SQL lexerRoot cause — original has duplicated, inconsistent skip logic in Rebind/In/compileNamedQuery. sqlex uses shared scanSkipSegment

Additional fixes: escaped ??/\? in Rebind, db:"-" skip in Named, missing field strict mode checks, NamedStmt.Exec return type, named parameter fallback tolerance (#892), and more.

Usage Examples

Basic CRUD

// Use ? placeholders universally — the framework auto-converts
// to target database bindvar format ($N, :argN, @pN)

// Insert
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "Alice", "alice@example.com")

// Query single row → struct
var user User
err = db.Get(&user, "SELECT * FROM users WHERE id = ?", 1)

// Query multiple rows → slice
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE age > ?", 18)

// Update
_, err = db.Exec("UPDATE users SET name = ? WHERE id = ?", "Alice Updated", 1)

// Delete
_, err = db.Exec("DELETE FROM users WHERE id = ?", 1)

Named Parameter Queries

// Using struct as parameter
user := User{Name: "Alice", Email: "alice@example.com"}
_, err = db.NamedExec(`INSERT INTO users (name, email) VALUES (:name, :email)`, user)

// Using map as parameter
params := map[string]any{"name": "Alice"}

// NamedGet — query single row
var result User
err = db.NamedGet(&result, `SELECT * FROM users WHERE name = :name`, params)

// NamedSelect — query multiple rows
var results []User
err = db.NamedSelect(&results, `SELECT * FROM users WHERE name = :name`, params)

// NamedQuery — return *Rows for manual iteration
rows, err := db.NamedQuery(`SELECT * FROM users WHERE name = :name`, params)
defer rows.Close()
for rows.Next() {
    var u User
    rows.StructScan(&u)
}

IN Queries

ids := []int{1, 2, 3, 4, 5}

// Positional: auto-detects slice and expands IN
var users []User
err = db.Select(&users, "SELECT * FROM users WHERE id IN (?)", ids)

// Named: built-in IN expansion
err = db.NamedSelect(&users,
    `SELECT * FROM users WHERE id IN (:ids) AND status = :status`,
    map[string]any{"ids": ids, "status": "active"})

Note: sqlex.In() / sqlex.Named() are legacy top-level functions; the framework calls them automatically. Use the high-level methods above which include Rebind/Hook/StrictMode.

Slice argument handling (IN list context recognition)

sqlex uses IN list context recognition to decide whether to auto-expand slices: slices are only expanded when ? is in the IN (?) context. Two conditions must be met:

  1. Strict (?) form: only one ? and optional ASCII whitespace (space/Tab/newline/CR) between ( and )
  2. The complete identifier immediately before ( is the IN keyword (case-insensitive); NOT IN (?) also matches

Other (?) contexts (ANY(?) / ALL(?) / VALUES (?) / func(?) / scalar subquery = (?) etc.) are treated as single values — no need for AsValue escape hatch.

Detection rules:

SQL patternArgumentBehaviorNotes
WHERE id IN (?)[]int{1,2,3}ExpandPreceded by IN
WHERE id NOT IN (?)[]int{1,2,3}ExpandNOT IN still matches
WHERE id IN (\n ?\n)[]int{1,2,3}ExpandMulti-line IN (?)
WHERE x = ANY(?)[]int{1,2,3}No expandPreceded by ANY, not IN
INSERT ... VALUES (?)[]int{1,2,3}No expandPreceded by VALUES, not IN
SELECT func(?)[]int{1,2,3}No expandPreceded by function name
WHERE x = (?)[]int{1,2,3}No expandPreceded by =, not IN
WHERE col_in (?)[]int{1,2,3}No expandFull token is col_in, not IN
IN (?, ?, ?)1, 2, 3 scalarsNo expandMultiple ? → user already expanded
WHERE x = ?[]int{1,2,3}No expand? not in (?) form

Escape hatch APIs:

import "github.com/go-sqlex/sqlex"

// ① sqlex.AsValue(v) — force no expansion (even in IN (?) context)
db.Select(&rows, `SELECT * FROM t WHERE id IN (?)`,
    sqlex.AsValue([]int{1, 2, 3})) // entire slice as single value to driver

// ② sqlex.AsList(slice) — force expansion (even outside IN (?) context)
db.Select(&rows, `SELECT * FROM t WHERE id = ANY(?)`,
    sqlex.AsList([]int{1, 2, 3})) // force expand to ?, ?, ?

// ③ Other native approaches still work
db.Exec(`INSERT INTO users (tags) VALUES (?)`, pq.Array([]int{1, 2, 3})) // driver.Valuer
data, _ := json.Marshal([]int{1, 2, 3})
db.Exec(`INSERT INTO t (json_col) VALUES (?)`, data) // []byte is a standard driver type

Note: ANY(?) / VALUES (?) etc. now default to no expansion — just pass the slice directly or wrap with pq.Array. No AsValue needed.

Priority (high to low):

  1. sqlex.AsValue(v) / sqlex.AsList(s) — explicit declaration, highest priority
  2. driver.Valuer interface (including pq.Array) — treated as single value
  3. []byte — standard driver type, treated as single value
  4. IN (?) context match + slice — auto-expand
  5. Other positions + slice — no expansion, passed as single value (driver will likely error)

Known edge case: A comment between IN and ( (e.g. IN /* c */ (?)) prevents IN recognition and won't expand. This pattern is extremely rare; use sqlex.AsList as a fallback if needed.

Empty slice handling (context-sensitive):

ScenarioBehavior
IN (?) context + []int{}Error sqlex: empty slice cannot be expanded into IN () (IN () is invalid SQL)
Non-IN context (WHERE x = ? / VALUES (?)) + []int{}OK, entire slice passed to driver
sqlex.AsValue([]int{})OK (already single-value semantics)
sqlex.AsList([]int{})Error sqlex.AsList: empty slice (expanding to nothing is meaningless)

Named parameter name rules & lexical context

Named parameter :name rule: [A-Za-z_][A-Za-z0-9_.]* (letter/underscore start, digits/underscore/dot allowed; dots for nested fields like :user.name).

PatternRecognized?Notes
:name / :user_id / :arg1Standard named parameter
:user.nameDot-nested field
:123 / :1❌ preserved as literalDigit-start rejected (avoids Oracle :N / SQLite ?NNN conflicts)
:名字 (Unicode)❌ preserved as literalASCII-only param names (matches db tag / map key convention)
::int (PG type cast)❌ preserved as literal:: recognized as type cast, not parameter
:= (assignment)❌ preserved as literalOutput as-is

Lexical scanning: :name / ? inside these regions are skipped (shared lexer.go scanner):

  • Single-quoted strings '...' (with '' escapes), double-quoted identifiers "...", backtick identifiers `...`
  • Dollar-quoted strings $$...$$ / $tag$...$tag$
  • Line comments -- ..., block comments /* ... */

If edge cases trigger a misparse, compileNamedQuery preserves unmatched :name as literals (same behavior as GORM's @name handling), allowing the original SQL to still execute correctly.

Prepared Statements

// Preparex auto-Rebinds — use ? uniformly across all databases
stmt, err := db.Preparex("SELECT * FROM users WHERE name = ?")
defer stmt.Close() // Stmt must be Closed to avoid resource leaks
var user User
err = stmt.Get(&user, "Alice")

// Also works within transactions
tx, _ := db.Beginx()
stmt, err = tx.Preparex("SELECT * FROM users WHERE age > ?")
defer stmt.Close()
var users []User
err = stmt.Select(&users, 18)

// PreparexContext — context-aware version
ctx := context.Background()
stmt, err = db.PreparexContext(ctx, "SELECT * FROM users WHERE name = ?")
defer stmt.Close()
var user2 User
err = stmt.Get(&user2, "Alice")

// PrepareNamed — named prepared statement (use :name uniformly, framework handles binding)
nstmt, err := db.PrepareNamed("SELECT * FROM users WHERE name = :name")
defer nstmt.Close()
err = nstmt.Get(&user, map[string]any{"name": "Alice"})

// PreparerContext — write generic prepare functions accepting DB/Tx/Conn
func prepareQuery(p sqlex.PreparerContext) (*sqlex.Stmt, error) {
    return sqlex.PreparexContext(context.Background(), p, "SELECT * FROM users WHERE name = ?")
}

Unified experience: Preparex/PreparexContext auto-Rebind like all other query methods, using ? uniformly. PrepareNamed/PrepareNamedContext use named parameters :name; the framework handles binding internally.

Note: Prepared statements (Stmt/NamedStmt) do not support IN slice expansion. The number of placeholders is fixed at Prepare time and cannot be dynamically expanded at execution time. For IN queries, use non-prepared methods like db.Select / db.NamedSelect.

Resource management: Stmt/NamedStmt hold an underlying sql.Stmt and must be Closed after use. Forgetting Close() causes prepared statement resource leaks in the connection pool that are only reclaimed on DB.Close(). Recommended pattern: defer stmt.Close().

Hook coverage: Stmt/NamedStmt Exec/Query methods also fire Hooks. Hooks are auto-propagated from the parent DB/Tx/Conn to the Stmt.

Transaction Management

// Recommended pattern: CloseWithErr auto-management
func createUserWithProfile(db *sqlex.DB, user User, profile Profile) (err error) {
    tx, err := db.Beginx()
    if err != nil {
        return err
    }
    defer func() { tx.CloseWithErr(err) }() // auto Commit or Rollback

    _, err = tx.NamedExec(`INSERT INTO users (name) VALUES (:name)`, user)
    if err != nil {
        return err // CloseWithErr detects err != nil, auto Rollback
    }

    _, err = tx.NamedExec(`INSERT INTO profiles (user_name, bio) VALUES (:user_name, :bio)`, profile)
    return nil // CloseWithErr detects err == nil, auto Commit
}

JSONValue[T]

import "github.com/go-sqlex/sqlex/types"

type Article struct {
    ID       int                            `db:"id"`
    Title    string                         `db:"title"`
    Metadata types.JSONValue[ArticleMeta]   `db:"metadata"`
}

type ArticleMeta struct {
    Tags      []string `json:"tags"`
    ViewCount int      `json:"view_count"`
}

// Write — auto-serializes to JSON
article := Article{
    Title:    "Hello World",
    Metadata: types.NewJSONValue(ArticleMeta{
        Tags:      []string{"go", "sql"},
        ViewCount: 0,
    }),
}
db.NamedExec(`INSERT INTO articles (title, metadata) VALUES (:title, :metadata)`, article)

// Read — auto-deserializes
var a Article
db.Get(&a, "SELECT * FROM articles WHERE id = ?", 1)
if a.Metadata.Valid {
    fmt.Println(a.Metadata.Val.Tags) // ["go", "sql"]
}
// Val is zero value when !Valid (guaranteed by Scan/zero-value init)
// Marshal/Unmarshal (implements json.Marshaler/Unmarshaler)
data, _ := json.Marshal(a.Metadata)
json.Unmarshal(data, &a.Metadata)

Hook Aspects

// Custom Hook — e.g., OpenTelemetry tracing
type TracingHook struct{}

func (h *TracingHook) BeforeQuery(ctx context.Context, event *sqlex.QueryEvent) context.Context {
    ctx, span := tracer.Start(ctx, "sql."+event.OperationType)
    span.SetAttributes(attribute.String("db.statement", event.Query))
    return ctx
}

func (h *TracingHook) AfterQuery(ctx context.Context, event *sqlex.QueryEvent) {
    span := trace.SpanFromContext(ctx)
    if event.Error != nil {
        span.RecordError(event.Error)
    }
    span.End()
}

db.AddHook(&TracingHook{})

// Hook covers the full lifecycle: query/exec/begin/commit/rollback
// Transaction operations (Begin/Commit/Rollback) also fire Hooks, regardless of success or failure
tx, _ := db.Beginx()       // → Hook(OpBegin)
// tx queries also fire Hooks
tx.CloseWithErr(nil)       // → Hook(OpCommit) or Hook(OpRollback)

QueryEvent Fields

type QueryEvent struct {
    Query         string        // SQL statement
    Args          []any         // execution parameters
    Duration      time.Duration // total elapsed time (includes Hook chain overhead)
    Error         error         // execution error (available in AfterQuery phase)
    OperationType OpType        // operation type: OpQuery/OpExec/OpBegin/OpCommit/OpRollback
    RowsAffected  int64         // rows affected (only for exec operations)
    LastInsertID  int64         // last inserted auto-increment ID (only for exec)
}

Conditional Hook Filtering

sqlex does not ship a built-in filter; use the decorator pattern to compose your own:

// Only fire on slow queries
func SlowOnly(h sqlex.Hook, threshold time.Duration) sqlex.Hook {
    return &slowHook{hook: h, threshold: threshold}
}
type slowHook struct {
    hook      sqlex.Hook
    threshold time.Duration
}
func (h *slowHook) BeforeQuery(ctx context.Context, e *sqlex.QueryEvent) context.Context {
    return h.hook.BeforeQuery(ctx, e)
}
func (h *slowHook) AfterQuery(ctx context.Context, e *sqlex.QueryEvent) {
    if e.Duration >= h.threshold {
        h.hook.AfterQuery(ctx, e)
    }
}

// Only fire on errors
func OnError(h sqlex.Hook) sqlex.Hook { /* BeforeQuery passthrough, AfterQuery checks e.Error != nil */ }

db.AddHook(SlowOnly(&AlertHook{}, 500*time.Millisecond))

StrictMode

// Default: lenient mode (strict=false), silently ignores extra columns
db, _ := sqlex.Connect("postgres", dsn)
fmt.Println(db.IsStrict()) // false

// Enable strict mode: returns detailed error on field mismatch
db.SetStrict(true)
err = db.Select(&users, "SELECT * FROM users")
// err: missing destination name email (index 2), age (index 3) in UserPartial

// strict auto-propagates to Tx/Conn
tx, _ := db.Beginx()    // inherits DB's strict setting
conn, _ := db.Connx(ctx) // inherits DB's strict setting

Unified Interfaces

DB, Tx, and Conn implement a common set of interfaces (enforced by compile-time assertions). Interfaces are small and orthogonal — compose as needed, no need for a "god interface":

InterfaceMethodsPurpose
ExtExec, Queryx, QueryRowxBasic query/execution
ExtContextExecContext, QueryxContext, QueryRowxContextContext-aware variants
NamedExtNamedExec, NamedQuery, NamedGet, NamedSelectNamed parameter queries
BindExtBindNamed, Get, Select, Rebind, DriverNamePositional parameter queries
PreparerPreparex, PrepareNamedPrepared statement creation
PreparerContextPreparexContext, PrepareNamedContextContext-aware preparation
// Accept DB, Tx, or Conn via NamedExt
func getUserByName(ext sqlex.NamedExt, name string) (*User, error) {
    var user User
    err := ext.NamedGet(&user, `SELECT * FROM users WHERE name = :name`,
        map[string]any{"name": name})
    return &user, err
}

user, err := getUserByName(db, "Alice")
tx, _ := db.Beginx()
user, err = getUserByName(tx, "Bob")
conn, _ := db.Connx(ctx)
user, err = getUserByName(conn, "Charlie")

Comparison with jmoiron/sqlx

Featurejmoiron/sqlxsqlex
Go version1.10+1.21+
Struct scanning
Named queries
Bindvar conversion✅ (enhanced: supports \? and ?? escape, skips string literals, identifiers, comments, PG dollar quoting)
IN clause expansionIn()✅ Auto-IN across all DB/Tx/Conn × Exec/Query/Select/Get/Named* paths
Cross-database placeholders❌ Manual Rebind✅ All methods auto-Rebind, use ? uniformly (including Preparex)
Field matchingunsafe (default strict)StrictMode (default lenient, more intuitive)
Hook aspectsAddHook pluggable SQL interceptors
JSONValue[T]types.JSONValue[T]
NamedGet/NamedSelect✅ DB/Tx convenience methods
CloseWithErr✅ Auto transaction management
Unified interfaces❌ DB/Tx methods overlap but no shared interfaceExt / ExtContext / NamedExt / BindExt / Preparer / PreparerContext — DB/Tx/Conn unified with compile-time checks
Unicode named parameters⚠️ Unreliable❌ Not supported (ASCII only; Unicode elsewhere is safe)
PostgreSQL ::❌ Misidentified✅ Correctly handled
Named query string literals❌ Colons misidentified✅ Skips colons in strings/comments
Named parameter fallback❌ Errors on misidentification✅ Missing params preserved as :name literals

Testing

# 1) Main package unit tests (no DB dependency, fastest)
go test -count=1 -timeout=120s .

# 2) cross_db MySQL only (isolate PG/SQLite for debugging)
SQLX_POSTGRES_DSN=skip SQLX_SQLITE_DSN=skip \
  go test -count=1 -timeout=300s ./tests/cross_db/

# 3) cross_db PostgreSQL only
SQLX_MYSQL_DSN=skip SQLX_SQLITE_DSN=skip \
  go test -count=1 -timeout=300s ./tests/cross_db/

# 4) cross_db SQLite only (no external dependencies)
SQLX_MYSQL_DSN=skip SQLX_POSTGRES_DSN=skip \
  go test -count=1 -timeout=120s ./tests/cross_db/

# 5) cross_db all drivers (CI recommended)
go test -count=1 -timeout=300s ./tests/cross_db/

# 6) integration tests
go test -count=1 -timeout=120s ./tests/integration/

# 7) pg-specific tests (PostgreSQL unique features)
go test -count=1 -timeout=120s ./tests/pg/

# 8) types / reflectx subpackages
go test -count=1 -timeout=60s ./types/ ./reflectx/

Why run per-driver: A single go test ./... runs all drivers at once, making it hard to isolate driver-specific failures. Per-driver runs enable quick bisection.

DSN configuration: Write complete DSNs in .env.test using the SQLX_*_DSN namespace. Set to skip to skip that driver. SQLite defaults to :memory:.

Env varValueBehavior
SQLX_MYSQL_DSNFull DSNUses this DSN
SQLX_MYSQL_DSNskip or emptySkips MySQL tests
SQLX_POSTGRES_DSNSame
SQLX_SQLITE_DSNSame (default :memory:)

Single test debugging

# Run a single test function
go test -count=1 -timeout=60s -run "TestNextPlaceholder" -v .

# Run a single sub-test
go test -count=1 -timeout=60s -run "TestNextPlaceholder/multiline_IN" -v .

# Race detection
go test -count=1 -race -timeout=180s .

# Coverage
go test -count=1 -cover -coverprofile=cover.out -timeout=120s .
go tool cover -html=cover.out

# Benchmarks
go test -bench=. -benchmem -run=NoSuch -benchtime=2s .

Performance

  • Prepared statements: Preparex/PreparexContext auto-Rebinds, unifying ? placeholders regardless of database driver
  • Zero-overhead principle: No Hook overhead when unregistered; auto-Rebind is a no-op for QUESTION-type drivers (MySQL/SQLite)
  • Auto Rebind: All query methods always perform Rebind. For MySQL/SQLite (already ?), Rebind returns the original string; for PostgreSQL etc., if the query has no ? (e.g., already $1), a fast path returns immediately. Double Rebind is safe and zero-cost
  • Slice arg detection: needsInRewrite uses reflection type checks (nanosecond-level for non-slice args)
  • Mapper caching: Field mapping results cached after first use
  • Hook execution: Hooks run synchronously; use lightweight operations or async for heavy ones

About NameMapper

NameMapper is a global variable that controls field-name-to-column-name mapping, defaulting to strings.ToLower.

Concurrency warning: NameMapper reads/writes are not concurrency-safe. Set it only in init(). Modifying at runtime may cause data races. For runtime per-instance mapping, use DB.MapperFunc().

License

MIT License

Based on jmoiron/sqlx — thanks to Jason Moiron for the excellent work.

Please read our Contributing Guide before submitting a pull request.
See CHANGELOG.md for version history.