qsql is a supplement to Go's database/sql package. It provides a DB type that embeds *sql.DB and intercepts Exec/Query/QueryRow (and their Context variants) to auto-convert ? placeholders to the format expected by the underlying database driver.
All query/exec methods accept ? as the placeholder — no need to use $1, @p1, or :1 manually.
type DB struct {
*sql.DB
// unexported: drvName string, isClose bool, mu sync.Mutex
}Embedded from *sql.DB AND overridden with placeholder conversion:
Exec,ExecContextQuery,QueryContextQueryRow,QueryRowContextBegin() (*Tx, error)— returns a wrapped*TxClose() errorDriverName() string
type Tx struct {
*sql.Tx
// unexported: drvName string
}Wraps *sql.Tx and overrides the same 6 methods with placeholder conversion.
Returned from DB.Begin().
type DBData struct { /* implements sql.Scanner */ }A universal scan target that converts any database column type to string.
Used by QueryPageArr/QueryDBDataArr/QueryPageMap/QueryDBDataMap.
type Txer interface {
Begin() (*Tx, error) // returns *Tx (has placeholder conversion)
}
type Execer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}
type Queryer interface {
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}
type QuickSql interface {
DriverName() string
InsertStruct(structPtr interface{}, tbName string) (sql.Result, error)
InsertStructContext(ctx context.Context, structPtr interface{}, tbName string) (sql.Result, error)
ScanStructs(rows *sql.Rows, structsPtr interface{}) error
QueryStruct(structPrt interface{}, querySql string, args ...interface{}) error
QueryStructContext(ctx context.Context, structPrt interface{}, querySql string, args ...interface{}) error
QueryStructs(structsPrt interface{}, querySql string, args ...interface{}) error
QueryStructsContext(ctx context.Context, structsPrt interface{}, querySql string, args ...interface{}) error
QueryElem(ePtr interface{}, querySql string, args ...interface{}) error
QueryElemContext(ctx context.Context, ePtr interface{}, querySql string, args ...interface{}) error
QueryElems(ePtr interface{}, querySql string, args ...interface{}) error
QueryElemsContext(ctx context.Context, ePtr interface{}, querySql string, args ...interface{}) error
QueryPageArr(querySql string, args ...interface{}) (titles []string, result [][]interface{}, err error)
QueryPageArrContext(ctx context.Context, querySql string, args ...interface{}) (titles []string, result [][]interface{}, err error)
QueryDBDataArr(querySql string, args ...interface{}) (titles []string, result [][]*DBData, err error)
QueryDBDataArrContext(ctx context.Context, querySql string, args ...interface{}) (titles []string, result [][]*DBData, err error)
QueryPageMap(querySql string, args ...interface{}) (titles []string, result []map[string]interface{}, err error)
QueryPageMapContext(ctx context.Context, querySql string, args ...interface{}) (titles []string, result []map[string]interface{}, err error)
QueryDBDataMap(querySql string, args ...interface{}) (titles []string, result []map[string]*DBData, err error)
QueryDBDataMapContext(ctx context.Context, querySql string, args ...interface{}) (titles []string, result []map[string]*DBData, err error)
StmtIn(paramStartIdx, paramLen int) string
Commit(func(tx *sql.Tx) error) error
}DB/Tx auto-converts ? in SQL strings before sending to the driver:
| Driver Constant | Driver String | Placeholder Format | Example |
|---|---|---|---|
DRV_NAME_MYSQL / DRV_NAME_SQLITE3 |
"mysql" / "sqlite3" |
? (unchanged) |
WHERE id = ? |
DRV_NAME_POSTGRES |
"postgres" |
$1, $2, ... |
WHERE id = $1 AND name = $2 |
DRV_NAME_ORACLE / _DRV_NAME_OCI8 |
"oracle" / "oci8" |
:1, :2, ... |
WHERE id = :1 AND name = :2 |
DRV_NAME_SQLSERVER / _DRV_NAME_MSSQL |
"sqlserver" / "mssql" |
@p1, @p2, ... |
WHERE id = @p1 AND name = @p2 |
| empty / unsupported | "" |
no conversion | SQL passed as-is |
Note: Does NOT parse SQL string literals. Avoid putting ? inside string literals like WHERE x = '?'.
DRV_NAME_MYSQL = "mysql"
DRV_NAME_ORACLE = "oracle" // alias: "oci8"
DRV_NAME_POSTGRES = "postgres"
DRV_NAME_SQLITE3 = "sqlite3" // alias: "sqlite" (modernc.org/sqlite)
DRV_NAME_SQLSERVER = "sqlserver" // alias: "mssql"// From an existing *sql.DB:
db := qsql.NewDB(drvName, sqlDB)
// From driver name + DSN (calls sql.Open internally):
db, err := qsql.Open(drvName, dsn)All accept Queryer interface. If the Queryer is a *DB, placeholder conversion happens automatically via the intercepted QueryContext/QueryRowContext.
qsql.QueryStruct(queryer, &structObj, "SELECT * FROM t WHERE id = ?", id)
qsql.QueryStructContext(ctx, queryer, &structObj, sql, args...)
qsql.QueryStructs(queryer, &sliceObj, "SELECT * FROM t WHERE id = ?", id)
qsql.QueryStructsContext(ctx, queryer, &sliceObj, sql, args...)
qsql.QueryElem(queryer, &result, "SELECT count(*) FROM t WHERE id = ?", id)
qsql.QueryElemContext(ctx, queryer, &result, sql, args...)
qsql.QueryElems(queryer, &slice, "SELECT name FROM t WHERE id = ?", id)
qsql.QueryElemsContext(ctx, queryer, &slice, sql, args...)
qsql.QueryPageArr(queryer, sql, args...) (titles []string, result [][]interface{}, err)
qsql.QueryPageArrContext(ctx, queryer, sql, args...)
qsql.QueryDBDataArr(queryer, sql, args...) (titles []string, result [][]*DBData, err)
qsql.QueryDBDataArrContext(ctx, queryer, sql, args...)
qsql.QueryPageMap(queryer, sql, args...) (titles []string, result []map[string]interface{}, err)
qsql.QueryPageMapContext(ctx, queryer, sql, args...)
qsql.QueryDBDataMap(queryer, sql, args...) (titles []string, result []map[string]*DBData, err)
qsql.QueryDBDataMapContext(ctx, queryer, sql, args...)qsql.InsertStruct(drvName, execer, &structObj, "table_name")
qsql.InsertStructContext(drvName, execer, ctx, &structObj, "table_name")qsql.ScanStructs(rows, &sliceOfStructs)// Commit runs a transaction. The callback receives bare *sql.Tx (no placeholder conversion).
// Use db.Begin() directly if you need placeholder conversion inside a transaction.
qsql.Commit(txer, func(tx *sql.Tx) error { ... })
// Lazy rollback (suppresses error in defer):
qsql.Rollback(tx)// Generate driver-specific placeholders for IN clauses:
// MySQL/SQLite3: ?,?,?
// PostgreSQL: $1,$2,$3
// Oracle: :1,:2,:3
// SQLServer: @p0,@p1,@p2
qsql.StmtIn(paramStartIdx, paramsLen int, drvName ...string) stringAll methods on *DB auto-convert ? placeholders:
db.QueryContext(ctx, "SELECT * FROM t WHERE id = ?", id)
db.QueryRowContext(ctx, "SELECT count(*) FROM t WHERE id = ?", id)
db.ExecContext(ctx, "UPDATE t SET x = ? WHERE id = ?", x, id)
// Also non-Context variants: Query, QueryRow, Execdb.InsertStruct(&structObj, "table_name")
db.InsertStructContext(ctx, &structObj, "table_name")
db.QueryStruct(&structObj, "SELECT * FROM t WHERE id = ?", id)
db.QueryStructContext(ctx, &structObj, sql, args...)
db.QueryStructs(&sliceObj, "SELECT * FROM t WHERE id = ?", id)
db.QueryStructsContext(ctx, &sliceObj, sql, args...)
db.QueryElem(&result, "SELECT count(*) FROM t WHERE id = ?", id)
db.QueryElemContext(ctx, &result, sql, args...)
db.QueryElems(&slice, "SELECT name FROM t WHERE id = ?", id)
db.QueryElemsContext(ctx, &slice, sql, args...)
db.QueryPageArr(sql, args...) (titles, [][]interface{}, error)
db.QueryPageArrContext(ctx, sql, args...)
db.QueryDBDataArr(sql, args...) (titles, [][]*DBData, error)
db.QueryDBDataArrContext(ctx, sql, args...)
db.QueryPageMap(sql, args...) (titles, []map[string]interface{}, error)
db.QueryPageMapContext(ctx, sql, args...)
db.QueryDBDataMap(sql, args...) (titles, []map[string]*DBData, error)
db.QueryDBDataMapContext(ctx, sql, args...)// Begin returns *Tx (has placeholder conversion on Exec/Query/QueryRow):
tx, err := db.Begin()
tx.ExecContext(ctx, "UPDATE t SET x = ? WHERE id = ?", x, id) // auto-converts ?
// Commit with callback (callback receives bare *sql.Tx, no conversion):
db.Commit(func(tx *sql.Tx) error { ... })
// Helpers:
db.StmtIn(paramStartIdx, paramsLen int) string*Tx embeds *sql.Tx and overrides the same 6 methods with placeholder conversion:
tx.ExecContext(ctx, "UPDATE t SET x = ? WHERE id = ?", x, id)
tx.QueryContext(ctx, "SELECT * FROM t WHERE id = ?", id)
tx.QueryRowContext(ctx, "SELECT count(*) FROM t WHERE id = ?", id)
// Also non-Context variants: Exec, Query, QueryRowSelectBuilder provides a chainable SQL builder. It has its own independent placeholder conversion.
bd := qsql.NewSelectBuilder(drvName)
bd.Select("id", "name")
bd.From("users")
bd.Where(true, "created_at BETWEEN ? AND ?", t1, t2)
bd.IfWhereIn(len(ids) > 0, "AND id IN ?", ids)
bd.OrderBy("id DESC")
bd.Offset(0)
bd.Limit(10)
sqlStr := bd.String() // returns SQL with driver-specific placeholders
args := bd.Args() // returns args
db.QueryContext(ctx, sqlStr, args...)Or use StrTo/Args with explicit driver name, and Sql() for (sqlStr, args) tuple:
sqlStr, args := bd.Sql()
titles, data, err := db.QueryDBDataArr(sqlStr, args...)Conditional variants: IfWhere, IfWhereIn, IfGroupBy, IfOrderBy, IfOffset, IfLimit — each takes a bool condition as the first argument.
// Register from ini file:
qsql.RegCacheWithIni("./etc/db.cfg")
// Register manually:
qsql.RegCache("main", db)
// Get cached DB:
db := qsql.GetCache("main") // panics if not found
db, err := qsql.HasCache("main") // returns error if not found
// Close all cached DBs:
qsql.CloseCache()Uses github.com/jmoiron/sqlx tag format with db:"field_name":
type User struct {
Id int64 `db:"id,auto_increment"` // auto_increment / autoincrement: skip on INSERT
Name string `db:"name"`
Ignore string `db:"-"` // "-": skip entirely
}-
Placeholder conversion is at the low level:
DBandTxoverrideExecContext/QueryContext/QueryRowContext(and non-Context variants). Any code calling these methods on a*DBor*Txgets automatic conversion — including the high-level query helpers. -
Commitcallback receives bare*sql.Tx: TheCommit(func(tx *sql.Tx) error)helper does not apply placeholder conversion inside the callback. Usedb.Begin()directly if you need conversion within a transaction. -
RollbackandCommitfunction signatures unchanged: These are transaction lifecycle helpers that don't touch SQL. -
Empty drvName = no conversion: If the driver name is not set or unrecognized, SQL is passed through unchanged.
-
Simple
?scan: The converter scans for?rune-by-rune without SQL parsing. Avoid?inside string literals or comments in SQL.
- Go
database/sqlstandard library github.com/jmoiron/sqlx(struct reflection and tag parsing)github.com/gwaylib/errors(error wrapping)